Category Archive : 前端技术

使用loki+promtail+grafana架构分析nginx日志

0.前提

1.已经阅读过这篇文章https://www.yinyubo.com/2022/03/14/nginx%E5%8A%A8%E6%80%81%E5%A2%9E%E5%8A%A0%E6%A8%A1%E5%9D%97ngx_http_geoip2_module

并且在nginx里已经安装好了geoip2

2.电脑上安装好了docker和docker-compose

1.调整nginx的访问日志格式

编辑/etc/nginx/nginx.conf,内容参考如下

...
load_module modules/ngx_http_geoip2_module.so;
...
http {
    include       /etc/nginx/mime.types;
    geoip2 /home/lzw/GeoLite2-Country_20220222/GeoLite2-Country.mmdb {
        auto_reload 5m;
        $geoip2_metadata_country_build metadata build_epoch;
        $geoip2_data_country_code default=CN source=$remote_addr country iso_code;
        $geoip2_data_country_name country names en;
    }
    geoip2 /home/lzw/GeoLite2-City_20220222/GeoLite2-City.mmdb {
        $geoip2_data_city_name default=Nanjing city names en;
 
    }
    vhost_traffic_status_zone;
    vhost_traffic_status_filter_by_set_key $geoip2_data_country_code country::*;
    log_format json_analytics escape=json '{'
                            '"msec": "$msec", ' # request unixtime in seconds with a milliseconds resolution
                            '"connection": "$connection", ' # connection serial number
                            '"connection_requests": "$connection_requests", ' # number of requests made in connection
                    '"pid": "$pid", ' # process pid
                    '"request_id": "$request_id", ' # the unique request id
                    '"request_length": "$request_length", ' # request length (including headers and body)
                    '"remote_addr": "$remote_addr", ' # client IP
                    '"remote_user": "$remote_user", ' # client HTTP username
                    '"remote_port": "$remote_port", ' # client port
                    '"time_local": "$time_local", '
                    '"time_iso8601": "$time_iso8601", ' # local time in the ISO 8601 standard format
                    '"request": "$request", ' # full path no arguments if the request
                    '"request_uri": "$request_uri", ' # full path and arguments if the request
                    '"args": "$args", ' # args
                    '"status": "$status", ' # response status code
                    '"body_bytes_sent": "$body_bytes_sent", ' # the number of body bytes exclude headers sent to a client
                    '"bytes_sent": "$bytes_sent", ' # the number of bytes sent to a client
                    '"http_referer": "$http_referer", ' # HTTP referer
                    '"http_user_agent": "$http_user_agent", ' # user agent
                    '"http_x_forwarded_for": "$http_x_forwarded_for", ' # http_x_forwarded_for
                    '"http_host": "$http_host", ' # the request Host: header
                    '"server_name": "$server_name", ' # the name of the vhost serving the request
                    '"request_time": "$request_time", ' # request processing time in seconds with msec resolution
                    '"upstream": "$upstream_addr", ' # upstream backend server for proxied requests
                    '"upstream_connect_time": "$upstream_connect_time", ' # upstream handshake time incl. TLS
                    '"upstream_header_time": "$upstream_header_time", ' # time spent receiving upstream headers
                    '"upstream_response_time": "$upstream_response_time", ' # time spend receiving upstream body
                    '"upstream_response_length": "$upstream_response_length", ' # upstream response length
                    '"upstream_cache_status": "$upstream_cache_status", ' # cache HIT/MISS where applicable
                    '"ssl_protocol": "$ssl_protocol", ' # TLS protocol
                    '"ssl_cipher": "$ssl_cipher", ' # TLS cipher
                    '"scheme": "$scheme", ' # http or https
                    '"request_method": "$request_method", ' # request method
                    '"server_protocol": "$server_protocol", ' # request protocol, like HTTP/1.1 or HTTP/2.0
                    '"pipe": "$pipe", ' # "p" if request was pipelined, "." otherwise
                    '"gzip_ratio": "$gzip_ratio", '
                    '"http_cf_ray": "$http_cf_ray",'
                    '"geoip_country_code": "$geoip2_data_country_code"'
                    '}';
    access_log   /var/log/nginx/json_access.log json_analytics;

2.安装loki+promtail+grafana

1.编写docker-compose文件,内容如下

version: "3"
 
networks:
  loki:
 
services:
  loki:
    image: grafana/loki:2.4.1
    ports:
      - "3100:3100"
    volumes:
      - /home/lzw/loki/loki-conf:/etc/loki
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - loki
 
  promtail:
    image: grafana/promtail:2.4.1
    volumes:
      - /home/lzw/loki/promtail-conf:/etc/promtail
      - /var/log/nginx:/var/log/nginx
    command: -config.file=/etc/promtail/config.yml
    networks:
      - loki
 
  grafana:
    image: grafana/grafana:latest
    volumes:
      - /home/lzw/loki/grafana:/var/lib/grafana
    ports:
      - "3000:3000"
    networks:
      - loki

2,编写loki-conf/local-config.yaml 配置文件

auth_enabled: false
 
server:
  http_listen_port: 3100
 
common:
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: inmemory
 
schema_config:
  configs:
    - from: 2020-10-24
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h
 
ruler:
  alertmanager_url: http://localhost:9093

3.编写promtail-conf/config.yml文件

server:
  http_listen_port: 9080
  grpc_listen_port: 0
 
positions:
  filename: /tmp/positions.yaml
 
clients:
  - url: http://loki:3100/loki/api/v1/push
 
scrape_configs:
- job_name: nginx
  static_configs:
  - targets:
      - localhost
    labels:
      job: nginx
      agent: promtail
      __path__: /var/log/nginx/json_access.log

4.准备grafana挂载目录

docker run -d -p 3002:3000 --name=grafana2 grafana/grafana:latest
docker cp grafana2:/var/lib/grafana /home/lzw/loki/.
docker rm -f grafana2
sudo chown -R 472 /home/lzw/loki/grafana

5.运行docker-compose

docker-compose -f docker-compose.yaml up -d

运行完成后,3000端口可以访问grafana、3100端口访问loki。nginx的日志文件通过volume的方式挂载进promtail

3.在grafana里配置报表

1.配置数据源http://loki:3100

2.导入官网模板https://grafana.com/grafana/dashboards/12559

3.导入后的效果应该和下图类似

nginx动态增加模块ngx_http_geoip2_module

0.前提:

1.已经阅读过我的另一篇动态增加nginx-module-vts模块的文章,服务器里已经安装了nginx和其源码。https://www.yinyubo.com/2022/03/14/apt%e6%96%b9%e5%bc%8f%e5%ae%89%e8%a3%85nginx%e4%bb%a5%e5%8f%8a%e5%8a%a8%e6%80%81%e5%a2%9e%e5%8a%a0%e6%a8%a1%e5%9d%97nginx-module-vts/

2.已经去GeoLite2的官网下载了GeoLite2-Country.mmdb文件,这个网站需要注册才能下载

3.可以参考的github网站有

https://github.com/leev/ngx_http_geoip2_module

https://github.com/maxmind/libmaxminddb

1.安装libmaxminddb


sudo add-apt-repository ppa:maxmind/ppa
sudo apt update
sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin

2.下载ngx_http_geoip2_module源码以及动态编译


# 下载ngx_http_geoip2_module源码
git clone https://github.com/leev/ngx_http_geoip2_module.git
#cd nginx源码目录,例如下面的命令
cd nginx-1.20.2/
# 进行动态编译
./configure --with-compat --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --add-dynamic-module=../ngx_http_geoip2_module --with-stream
make modules
cp objs/ngx_http_geoip2_module.so /etc/nginx/modules/.

3.在nginx配置文件里引入geoip2

这里我以在nginx-module-vts里加入geoip2地区解析为例,修改nginx.conf

...
load_module modules/ngx_http_vhost_traffic_status_module.so;
load_module modules/ngx_http_geoip2_module.so;
...
http {
    ...
    geoip2 /home/lzw/GeoLite2-Country_20220222/GeoLite2-Country.mmdb {
        auto_reload 5m;
        $geoip2_metadata_country_build metadata build_epoch;
        $geoip2_data_country_code default=CN source=$remote_addr country iso_code;
        $geoip2_data_country_name country names es;
    }
    geoip2 /home/lzw/GeoLite2-City_20220222/GeoLite2-City.mmdb {
        $geoip2_data_city_name default=Nanjing city names en;
        $geoip2_data_latitude location latitude;
        $geoip2_data_longitude location longitude;
        $geoip2_data_postalcode postal code;
    }
    default_type  application/octet-stream;
    vhost_traffic_status_zone;
    vhost_traffic_status_filter_by_set_key $geoip2_data_country_code country::*;
    vhost_traffic_status_filter_by_set_key $geoip2_data_city_name city::*;
    vhost_traffic_status_filter_by_set_key "$geoip2_data_latitude,$geoip2_data_longitude" latlong::*;
    vhost_traffic_status_filter_by_set_key $geoip2_data_longitude longitude::*;
    vhost_traffic_status_filter_by_set_key $geoip2_data_latitude latitude::*;
    vhost_traffic_status_filter_by_set_key $geoip2_data_postalcode postal::*;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
 
    access_log  /var/log/nginx/access.log  main;
    ...
}

在每一个被监控的conf文件里 添加vhost_traffic_status_filter_by_set_key 信息,例如下面我添加了6个筛选项


server {
    listen       5320;
    server_name  localhost;    
    vhost_traffic_status_filter_by_set_key $geoip2_data_country_code country::$server_name;
    vhost_traffic_status_filter_by_set_key $geoip2_data_city_name city::$server_name;
    vhost_traffic_status_filter_by_set_key "$geoip2_data_latitude,$geoip2_data_longitude" latlong::$server_name;
    vhost_traffic_status_filter_by_set_key $geoip2_data_longitude longitude::$server_name;
    vhost_traffic_status_filter_by_set_key $geoip2_data_latitude latitude::$server_name;
    vhost_traffic_status_filter_by_set_key $geoip2_data_postalcode postal::$server_name;  
 
    location /status {
        vhost_traffic_status_display;
        vhost_traffic_status_display_format html;
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

apt方式安装nginx以及动态增加模块nginx-module-vts

0.背景介绍

因为有很多人是先通过apt的访问安装了稳定版的nginx。后面突然要增加第三方模块如geoip或者nginx-module-vts等别的模块,这个时候就可以采用本文的方式去动态增加模块。

1.安装ubuntu(如果已经安装了,可以跳过)

# 安装必要工具
sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring

# 导入官方Nginx签名密钥
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
    | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null

# 验证下载的文件中包含正确的密钥
gpg --dry-run --quiet --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg

# 设置稳定版本的nginx仓库
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu `lsb_release -cs` nginx" \
    | sudo tee /etc/apt/sources.list.d/nginx.list

# 通过apt安装nginx
sudo apt update
sudo apt install nginx

2.确认安装的nginx版本信息

输入 nginx -V 检查回显


nginx version: nginx/1.20.2
built by gcc 9.3.0 (Ubuntu 9.3.0-10ubuntu2)
built with OpenSSL 1.1.1f  31 Mar 2020
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-g -O2 -fdebug-prefix-map=/data/builder/debuild/nginx-1.20.2/debian/debuild-base/nginx-1.20.2=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie'

通过上述回显,我们可以看到nginx的配置文件路径和程序路径如下。这个路径用在后面的动态编译里

--prefix=/etc/nginx --sbin-path=/usr/sbin/nginx

3.下载nginx源代码到本地

因为我们之前的nginx是apt安装的,本地没有源码,而动态编译模块需要源码,所以这里下载源码到本地

进入http://nginx.org/en/download.html,下载与我们上面apt安装的nginx同版本的tar.gz包,例如这里我们装的是1.20.2版本

解压nginx源代码
tar -zxvf nginx-1.20.2.tar.gz

4.下载第三方模块nginx-module-vts到本地

git clone git://github.com/vozlt/nginx-module-vts.git

下载完成后,两个目录同级,例如下面的目录

目录
|---nginx-1.20.2
|---nginx-module-vts

5.进行编译

安装编译所需要的lib库

sudo apt install g++ gcc libpcre3 libpcre3-dev zlib1g-dev openssl libssl-dev make

进入nginx-1.20.2目录

cd nginx-1.20.2

使用configure工具进行编译。编译完成后会在objs目录下生成文件ngx_http_vhost_traffic_status_module.so,把这个ngx_http_vhost_traffic_status_module.so拷贝到/etc/nginx/modules/目录下

./configure --with-compat --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --add-dynamic-module=../nginx-module-vts
make modules
sudo cp objs/ngx_http_vhost_traffic_status_module.so /etc/nginx/modules/.

6.在nginx里加载第三方模块nginx-module-vts

编辑nginx.conf文件.

在文件顶部添加load_module modules/ngx_http_vhost_traffic_status_module.so;

在http下添加vhost_traffic_status_zone;

cd /etc/nginx/
sudo nano nginx.conf
#  以下是文件内容
...
load_module modules/ngx_http_vhost_traffic_status_module.so;
...
http {
    vhost_traffic_status_zone;
}

在/etc/nginx/conf.d目录下增加一个monitor.conf。主要添加vhost_traffic_status_display;和vhost_traffic_status_display_format html; 参考如下

server {
    listen       5320;
    server_name  localhost;
 
    #access_log  /var/log/nginx/host.access.log  main;
 
    location /status {
        vhost_traffic_status_display;
        vhost_traffic_status_display_format html;
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

添加完成后,输入sudo systemctl restart nginx重启nginx

访问monitor.conf里对应的端口  http://服务器IP:5320/ 即可看到流量信息

一篇文章带你入门K8S二次开发

背景

我们经常会在网上看到K8S和周边工具的教程,例如HELM的使用,droneCI的使用,但是很少有文章写,如何基于K8S进行二次开发,本篇文章将使用python和vue进行K8S的二次开发,实现一个简单的查询k8s的pod和node信息的页面


效果图

通过前端页面,调用后端python接口,查询k8s当前的节点状态和应用状态


涉及到的知识点

知识点说明
python-sanic库为前台提供API接口
python-kubernetes库访问k8s,获取pod和node资源信息
nodejs-vue前端框架
nodejs-element-UI提供UI组件,用了图标和表格组件
k8s-helm程序最后是要运行在K8S里,所以要编写helm包,包括rbac,svc,deployment文件
docker前后端的docker镜像制作

用户故事


后端python代码解说

main.py 主函数入口

#main.py
from kubernetes import client, config
from sanic import Sanic
from sanic.response import json

from cors import add_cors_headers
from options import setup_options

# sanic程序必须有个名字
app = Sanic("backend")
# 在本地调试,把config文件拷贝到本机的~/.kube/config然后使用load_kube_config,在K8S集群里使用load_incluster_config
# config.load_kube_config()
config.load_incluster_config()


def check_node_status(receiver):
    '''
    检查节点的状态是否正确,正确的设为1,不正确的设为0
    '''
    # 期望结果
    expect = {"NetworkUnavailable": "False",
              "MemoryPressure": "False",
              "DiskPressure": "False",
              "PIDPressure": "False",
              "Ready": "True"
              }
    result_dict = {}
    for (key, value) in receiver.items():
        # 这个逻辑是判断k8s传过来的值与expect的值是否相同
        if expect[key] == value:
            result_dict[key] = 1
        else:
            result_dict[key] = 0
    return result_dict


@app.route("/api/node")
async def node(request):
    result = []
    v1 = client.CoreV1Api()
    node_rest = v1.list_node_with_http_info()
    for i in node_rest[0].items:
        computer_ip = i.status.addresses[0].address
        computer_name = i.status.addresses[1].address
        # 先获得节点的IP和名字
        info = {"computer_ip": computer_ip, "computer_name": computer_name}
        status_json = {}
        # 节点有多个状态,把所有状态查出来,存入json里
        # 这里有一个flannel插件的坑,及时节点关机了,NetworkUnavailable查出来还是False
        for node_condition in i.status.conditions:
            status_json[node_condition.type] = node_condition.status
        check_dict = check_node_status(status_json)
        # 把节点的状态加入节点信息json里
        info.update(check_dict)
        # 把每一个节点的查询结果加入list里,返回给前端
        result.append(info)
    return json(result)


@app.route("/api/pod")
async def pod(request):
    '''
    接口名是pod,其实是检查所有的deployment,statefulset,daemonset的副本状态
    通过这些状态判断当前的程序是否正常工作
    '''
    pod_list = []
    apis_api = client.AppsV1Api()
    # 检查deployment信息
    resp = apis_api.list_deployment_for_all_namespaces()
    for i in resp.items:
        pod_name = i.metadata.name
        pod_namespace = i.metadata.namespace
        pod_unavailable_replicas = i.status.unavailable_replicas
        # 不可用副本状态为None表示没有不可用的副本,程序正常
        if pod_unavailable_replicas == None:
            pod_status = 1
        else:
            pod_status = 0
        pod_json = {"pod_namespace": pod_namespace, "pod_name": pod_name, "pod_status": pod_status}
        pod_list.append(pod_json)
    # 检查stateful_set信息
    resp_stateful = apis_api.list_stateful_set_for_all_namespaces()
    for i in resp_stateful.items:
        pod_name = i.metadata.name
        pod_namespace = i.metadata.namespace
        # 正常工作的副本数量,等于期望的副本数量时,表明程序是可用的
        if i.status.ready_replicas == i.status.replicas:
            pod_status = 1
        else:
            pod_status = 0
        pod_json = {"pod_namespace": pod_namespace, "pod_name": pod_name, "pod_status": pod_status}
        pod_list.append(pod_json)
    # 检查daemonset信息
    resp_daemonset = apis_api.list_daemon_set_for_all_namespaces()
    for i in resp_daemonset.items:
        pod_name = i.metadata.name
        pod_namespace = i.metadata.namespace
        # 不可用副本状态为None表示没有不可用的副本,程序正常
        if i.status.number_unavailable == None:
            pod_status = 1
        else:
            pod_status = 0
        pod_json = {"pod_namespace": pod_namespace, "pod_name": pod_name, "pod_status": pod_status}
        pod_list.append(pod_json)
    return json(pod_list)


# Add OPTIONS handlers to any route that is missing it
app.register_listener(setup_options, "before_server_start")

# Fill in CORS headers
app.register_middleware(add_cors_headers, "response")
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

cors.py 解决跨域问题,主要是本地调试方便,放到我的helm包里部署到K8S上时,是不需要的,因为我会用nginx把他反向代理过去

#cors.py
from typing import Iterable


def _add_cors_headers(response, methods: Iterable[str]) -> None:
    '''
    为了在测试的时候偷懒,我把Access-Control-Allow-Origin设置成了*
    如果是做成镜像和我的helm包一起用,是不需要这样的,因为我会用nginx把后端和前端设置成同源
    '''
    allow_methods = list(set(methods))
    if "OPTIONS" not in allow_methods:
        allow_methods.append("OPTIONS")
    headers = {
        "Access-Control-Allow-Methods": ",".join(allow_methods),
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Credentials": "true",
        "Access-Control-Allow-Headers": (
            "origin, content-type, accept, "
            "authorization, x-xsrf-token, x-request-id"
        ),
    }
    response.headers.extend(headers)


def add_cors_headers(request, response):
    if request.method != "OPTIONS":
        methods = [method for method in request.route.methods]
        _add_cors_headers(response, methods)

options.py 搭配上面的cors.py使用

# options.py
from collections import defaultdict
from typing import Dict, FrozenSet

from sanic import Sanic, response
from sanic.router import Route

from cors import _add_cors_headers


def _compile_routes_needing_options(
        routes: Dict[str, Route]
) -> Dict[str, FrozenSet]:
    needs_options = defaultdict(list)
    # This is 21.12 and later. You will need to change this for older versions.
    for route in routes.values():
        if "OPTIONS" not in route.methods:
            needs_options[route.uri].extend(route.methods)

    return {
        uri: frozenset(methods) for uri, methods in dict(needs_options).items()
    }


def _options_wrapper(handler, methods):
    def wrapped_handler(request, *args, **kwargs):
        nonlocal methods
        return handler(request, methods)

    return wrapped_handler


async def options_handler(request, methods) -> response.HTTPResponse:
    resp = response.empty()
    _add_cors_headers(resp, methods)
    return resp


def setup_options(app: Sanic, _):
    app.router.reset()
    needs_options = _compile_routes_needing_options(app.router.routes_all)
    for uri, methods in needs_options.items():
        app.add_route(
            _options_wrapper(options_handler, methods),
            uri,
            methods=["OPTIONS"],
        )
    app.router.finalize()

requirements.txt 放置python需要用到的sdk

aiofiles==0.8.0
cachetools==4.2.4
certifi==2021.10.8
charset-normalizer==2.0.10
google-auth==2.3.3
httptools==0.3.0
idna==3.3
Jinja2==3.0.3
kubernetes==21.7.0
MarkupSafe==2.0.1
multidict==5.2.0
oauthlib==3.1.1
pyasn1==0.4.8
pyasn1-modules==0.2.8
python-dateutil==2.8.2
PyYAML==6.0
requests==2.27.1
requests-oauthlib==1.3.0
rsa==4.8
sanic==21.12.1
sanic-ext==21.12.3
sanic-routing==0.7.2
six==1.16.0
urllib3==1.26.8
websocket-client==1.2.3
websockets==10.1

Dockerfile 打包后端代码成镜像使用 docker build -t k8s-backend .

FROM python:3.9
ADD . .
RUN pip install -r /requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
WORKDIR .
CMD ["python3","main.py"]

前端代码解说

主要就是App.vue和main.ts两个文件

这里省略nodejs和vue的安装过程,使用下面的命令创建一个vue3的项目

# 下载vue的过程省略,创建一个vue项目,创建的时候,选择typescript版本
vue create k8s-frontend
# 安装element-ui的vue3版本
npm install element-plus --save
# npm安装axios,用于向后台发起请求
npm i axios -S

main.ts 主入口

import { Component, createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)

app.use(ElementPlus)

app.mount('#app')

App.vue 做demo图省事,我就用了这一个vue文件放了所有功能

<template>

  <h2>服务器信息</h2>
  <el-table 
    :data="tableData" style="width: 100%">
    <el-table-column prop="computer_ip" label="IP地址" width="180" />
    <el-table-column prop="computer_name" label="服务器名字" width="180" />
    <el-icon><check /></el-icon>
    <el-table-column label="网络插件" width="100">
      <template #default="scope">
        <el-icon :size="20">
          <check class="check" v-if="scope.row.NetworkUnavailable ==1" />
          <close class="close" v-else />
        </el-icon>
      </template>
    </el-table-column>
    <el-table-column label="内存压力" width="100">
      <template #default="scope">
        <el-icon :size="20">
          <check class="check" v-if="scope.row.MemoryPressure ==1" />
          <close class="close" v-else />
        </el-icon>
      </template>
    </el-table-column>
    <el-table-column label="硬盘压力" width="100">
      <template #default="scope">
        <el-icon :size="20">
          <check class="check" v-if="scope.row.DiskPressure ==1" />
          <close class="close" v-else />
        </el-icon>
      </template>
    </el-table-column>
    <el-table-column label="进程压力" width="100">
      <template #default="scope">
        <el-icon :size="20">
          <check class="check" v-if="scope.row.PIDPressure ==1" />
          <close class="close" v-else />
        </el-icon>
      </template>
    </el-table-column>
    <el-table-column label="K3S状态" width="100">
      <template #default="scope">
        <el-icon :size="20">
          <check class="check" v-if="scope.row.Ready ==1" />
          <close class="close" v-else />
        </el-icon>
      </template>
    </el-table-column>
  </el-table>
  <el-divider></el-divider>
  <h2>应用程序信息</h2>
  <el-table
    :data="podData"
    style="width: 100%"
    :default-sort="{ prop: 'pod_status', order: 'ascending' }"
  >
    <el-table-column prop="pod_namespace" sortable  label="命名空间" width="180" />
    <el-table-column prop="pod_name" sortable label="应用名字" width="180" />
    <el-icon><check /></el-icon>
    <el-table-column prop="pod_status" label="是否正常" sortable width="100">
      <template #default="scope">
        <el-icon :size="20">
          <check class="check" v-if="scope.row.pod_status ==1" />
          <close class="close" v-else />
        </el-icon>
      </template>
    </el-table-column>
  </el-table>
  <el-divider></el-divider>
</template>

<script lang="ts" >
import { Options, Vue } from 'vue-class-component';
import { Check, Close } from '@element-plus/icons-vue';
import axios from 'axios'

@Options({
    // 这里可以配置Vue组件支持的各种选项
    components: {
        Check,
        Close
    },
    data() {
        return {
          podData: [],
          tableData: [],
        }
    },
    mounted() {
      this.pod();
      this.show();
    },
    methods: {
        say(){
          console.log("say");
        },
        pod(){
          const path = "http://127.0.0.1:8000/api/pod";
          //本地调试使用,在服务器上还是用相对路径
          // const path = "http://127.0.0.1:8000/node";
          // 务必使用箭头函数的方法,这样this.id能直接对上,不然会报错提示id没找到
          axios.get(path).then((response) => {
            this.podData = response.data;
          });
        },
        show() {
        const path = "http://127.0.0.1:8000/api/node";
        //本地调试使用,在服务器上还是用相对路径
        // const path = "http://127.0.0.1:8000/node";
        // 务必使用箭头函数的方法,这样this.id能直接对上,不然会报错提示id没找到
        axios.get(path).then((response) => {
          this.tableData = response.data;
        });
      },
    }
})
export default class App extends Vue {
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: left;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Dockerfile 用于制作前端镜像 docker build -t k8s-frontend .

FROM  node:14-alpine3.12 AS build

LABEL maintainer="sunj@sfere-elec.com"

ADD . /build/

RUN set -eux \
    && yarn config set registry https://mirrors.huaweicloud.com/repository/npm/ \
    && yarn config set sass_binary_site https://mirrors.huaweicloud.com/node-sass \
    && yarn config set python_mirror https://mirrors.huaweicloud.com/python \
    && yarn global add yrm \
    && yrm add sfere http://repo.sfere.local:8081/repository/npm-group/ \
    && yrm use sfere \
    && cd /build \
    && yarn install \
    && yarn build

FROM nginx:1.21.5-alpine
LABEL zhenwei.li "zhenwei.li@sfere-elec.com"
COPY --from=build /build/dist/ /usr/share/nginx/html
# 暴露端口映射
EXPOSE 80

HELM包解说

deployment.yaml 把两个docker镜像放在同一个deployment里

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: {{ .Release.Name }}
  name: {{ .Release.Name }}
spec:
  replicas: 1
  revisionHistoryLimit: 5
  selector:
    matchLabels:
      app: {{ .Release.Name }}
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}
    spec:
      containers:
        - image: k8s-check-backend
          imagePullPolicy: Always
          name: server-check-backend
          resources: {}
        - image: k8s-check-frontend
          imagePullPolicy: Always
          name: server-check-frontend
          resources: {}
          volumeMounts:
          - name: nginx-conf
            mountPath: /etc/nginx/conf.d/default.conf
            subPath: default.conf
      restartPolicy: Always
      volumes:
        - name: nginx-conf
          configMap:
            name: {{ .Release.Name }}
            items:
            - key: default.conf
              path: default.conf
      serviceAccountName: {{ .Release.Name }}

service.yaml 把前端通过nodeport方式暴露出去,方便测试

apiVersion: v1
kind: Service
metadata:
  labels:
    app: {{ .Release.Name }}
  name: {{ .Release.Name }}
spec:
  type: NodePort
  ports:
    - name: web
      port: 80
      targetPort: 80
      nodePort: 32666
  selector:
    app: {{ .Release.Name }}

configmap.yaml nginx的配置文件,反向代理后端

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}
data:
  default.conf: |
    # 当前项目nginx配置文件,lzw
    server {
        listen       80;
        server_name  _A;
        gzip on;
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   /usr/share/nginx/html;
        }

        location / {
            root /usr/share/nginx/html;
            index index.html index.htm;

            if (!-e $request_filename){
                    rewrite ^/.* /index.html last;
            }
        }
        location /api {
            proxy_pass          http://localhost:8000;
            proxy_http_version 1.1;
            proxy_set_header    X-Real-IP           $remote_addr;
            proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
        }

        error_page   500 502 503 504  /50x.html;
    }

rbac.yaml 我们的程序是需要访问k8s资源的,如果没有配置rbac,调用K8S的API会报403错误

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/name: {{ .Release.Name }}
  name: {{ .Release.Name }}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: {{ .Release.Name }}
subjects:
- kind: ServiceAccount
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: {{ .Release.Name }}
  name: {{ .Release.Name }}
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  - secrets
  - nodes
  - pods
  - services
  - resourcequotas
  - replicationcontrollers
  - limitranges
  - persistentvolumeclaims
  - persistentvolumes
  - namespaces
  - endpoints
  verbs:
  - list
  - watch
- apiGroups:
  - extensions
  resources:
  - daemonsets
  - deployments
  - replicasets
  - ingresses
  verbs:
  - list
  - watch
- apiGroups:
  - apps
  resources:
  - statefulsets
  - daemonsets
  - deployments
  - replicasets
  verbs:
  - list
  - watch
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/name: {{ .Release.Name }}
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}

程序安装

helm install k8s-check helm/k8s-server-check

安装完成后,通过http://masterip:32666访问即可

electron+droneCI+minio流水线

背景

因为我们的electron程序已经开发完成,期望要能开发人员每次上传代码,打了tag就自动build一份deb文件,自动上传到minio,方便运维人员去拿deb文件部署到ubuntu环境上。我们已有的技术栈包含droneCI,minio,python,于是边有了该方案。本文省略了vault,ldap,minio,harbor的安装与配置,这些程序的安装配置在本网站的其他文章里,就不一一贴出来了


架构图

解释:

1.前端开发上传electron代码到git服务端

2.git服务端通过webhook方式通知drone-server产生了。例如本文只测试的是发布tag触发webhook,还有很多种触发方式都可以设置

3.drone-server收到通知后,再在drone-runner所在的k8s集群里启动一个包含nodejs和python的任务容器

4.任务容器通过electron-forge make 命令打包一个deb文件

5.任务容器通过minio提供的python sdk上传deb文件到minio


drone插件编写

要完成上述目标,第一步就是得编写一个drone的插件

我编写该插件使用的是nodejs16版本的debian系统,然后通过提前安装好需要的如下表格里的工具。注意,因为我用的是华为源,2021年12月9日的时候,华为镜像上最新的electron只到16.0.2版本,所以注意指定版本号

介绍:该插件使用nodejs16版本的debian系统,然后通过提前安装好需要的如下工具。注意,因为我用的是华为源,2021年12月9日的时候,华为镜像上最新的electron只到16.0.2版本,所以注意指定版本号

工具名
rpm
python3-pip
python3
fakeroot
electron@v16.0.2 
electron-prebuilt-compile
electron-forge 
dpkg
minio的python sdk

代码有3个文件main.py Dockerfile ,requirements.txt,下面是详细介绍

main.py

代码功能是先获取环境变量,然后使用git的tag号替换掉package.json里的version字段。执行yarn install,yanr make,通过环境变量找到需要上传的文件,通过pythonde的sdk上传到minio里。详细代码如下

#main.py
import json
import os
import subprocess

from minio import Minio
from minio.error import S3Error

endpoint = "minio.sfere.local"
access_key = "bababa"
secret_key = "bababa"
bucket = "electronjs"
folder_path = "/drone/src/out/make/deb/x64"
suffix = "deb"
tag = "0.0.0"


def find_file_by_suffix(target_dir, target_suffix="deb"):
    find_res = []
    target_suffix_dot = "." + target_suffix
    walk_generator = os.walk(target_dir)
    for root_path, dirs, files in walk_generator:
        if len(files) < 1:
            continue
        for file in files:
            file_name, suffix_name = os.path.splitext(file)
            if suffix_name == target_suffix_dot:
                find_res.append(os.path.join(root_path, file))
    return find_res


def get_environment():
    global endpoint, access_key, secret_key, bucket, suffix, tag

    if "PLUGIN_ENDPOINT" in os.environ:
        endpoint = os.environ["PLUGIN_ENDPOINT"]
    if "PLUGIN_ACCESS_KEY" in os.environ:
        access_key = os.environ["PLUGIN_ACCESS_KEY"]
    if "PLUGIN_SECRET_KEY" in os.environ:
        secret_key = os.environ["PLUGIN_SECRET_KEY"]
    if "PLUGIN_BUCKET" in os.environ:
        bucket = os.environ["PLUGIN_BUCKET"]
    if "PLUGIN_SUFFIX" in os.environ:
        suffix = os.environ["PLUGIN_SUFFIX"]
    if "PLUGIN_TAG" in os.environ:
        tag = os.environ["PLUGIN_TAG"]


def yarn_make():
    with open('./package.json', 'r', encoding='utf8')as fp:
        json_data = json.load(fp)
    json_data['version'] = tag
    with open('./package.json', 'w', encoding='utf8')as fp:
        json.dump(json_data, fp, ensure_ascii=False, indent=2)
    print('package version replace to ' + tag)
    print(subprocess.run("yarn install", shell=True))
    print(subprocess.run("yarn make", shell=True))


def upload_file():
    file_list = find_file_by_suffix(folder_path, suffix)
    # 创建minio连接,这里因为我们是http的,所以secure=False
    client = Minio(
        endpoint=endpoint,
        access_key=access_key,
        secure=False,
        secret_key=secret_key,
    )

    # 检查bucket是否存在,不存在就创建bucket
    found = client.bucket_exists(bucket)
    if not found:
        client.make_bucket(bucket)
    else:
        print("Bucket 'electronjs' already exists")

    # 上传文件到bucket里
    for file in file_list:
        name = os.path.basename(file)
        client.fput_object(
            bucket, name, file,
        )
        print(
            "'" + file + "' is successfully uploaded as "
                         "object '" + name + "' to bucket '" + bucket + "'."
        )


if __name__ == "__main__":
    get_environment()
    yarn_make()
    try:
        upload_file()
    except S3Error as exc:
        print("error occurred.", exc)

Dockerfile

取一个node16版本的debian系统,使用国内源安装我们在之前列出来要用的工具,然后指定程序入口是我们的python程序。编写完后,使用docker build -t drone-electron-minio-plugin:0.1.0 . 做一个镜像上传到私仓里

FROM node:16-buster
RUN npm config set registry https://mirrors.huaweicloud.com/repository/npm/ \
    && npm config set disturl https://mirrors.huaweicloud.com/nodejs \
    && npm config set sass_binary_site https://mirrors.huaweicloud.com/node-sass \
    && npm config set phantomjs_cdnurl https://mirrors.huaweicloud.com/phantomjs \
    && npm config set chromedriver_cdnurl https://mirrors.huaweicloud.com/chromedriver \
    && npm config set operadriver_cdnurl https://mirrors.huaweicloud.com/operadriver \
    && npm config set electron_mirror https://mirrors.huaweicloud.com/electron/ \
    && npm config set python_mirror https://mirrors.huaweicloud.com/python \
    && npm config set canvas_binary_host_mirror https://npm.taobao.org/mirrors/node-canvas-prebuilt/ \
    && npm install -g npm@8.2.0 \
    && yarn config set registry https://mirrors.huaweicloud.com/repository/npm/ \
    && yarn config set disturl https://mirrors.huaweicloud.com/nodejs \
    && yarn config set sass_binary_site https://mirrors.huaweicloud.com/node-sass \
    && yarn config set phantomjs_cdnurl https://mirrors.huaweicloud.com/phantomjs \
    && yarn config set chromedriver_cdnurl https://mirrors.huaweicloud.com/chromedriver \
    && yarn config set operadriver_cdnurl https://mirrors.huaweicloud.com/operadriver \
    && yarn config set electron_mirror https://mirrors.huaweicloud.com/electron/ \
    && yarn config set python_mirror https://mirrors.huaweicloud.com/python \
    && yarn config set canvas_binary_host_mirror https://npm.taobao.org/mirrors/node-canvas-prebuilt/ \
    && yarn global add electron@v16.0.2 electron-forge electron-prebuilt-compile\
    && sed -i "s@http://ftp.debian.org@https://repo.huaweicloud.com@g" /etc/apt/sources.list \
    && sed -i "s@http://security.debian.org@https://repo.huaweicloud.com@g" /etc/apt/sources.list \
    && sed -i "s@http://deb.debian.org@https://repo.huaweicloud.com@g" /etc/apt/sources.list \
    && apt update \
    && apt install -y fakeroot dpkg rpm python3 python3-pip
ADD . .   
WORKDIR . 
RUN pip3 install -r ./requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
#CMD ["python3","/main.py"]
WORKDIR /drone/src
ENTRYPOINT ["python3", "/main.py"]

requirements.txt

minio==7.1.2

electron仓库代码

我们的electron仓库里要添加一个.drone.yml文件和对package.json稍微进行一些修改

package.json

.drone.yml

droneCI的流水线文件,使用了我们在上一节里build出来的drone插件镜像


流水线演示

需要人手动操作的

流水线自动操作的

基于istio的灰度发布实验

背景

灰度发布又叫A/B测试,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。
因为最近刚好有灰度发布的需求,我又学了一遍istio,记录了本次灰度发布的实施过程(只包括应用,不包括数据库升级)


实验过程

  1. 先确定目前的应用版本为V1
  2. 通过helm包部署应用版本为V2的pod到K8S集群中
  3. 确定V2版本灰度的用户,方法包括IP,或者特定用户
  4. 通过istio的virtualservice功能把特定用户的流量指向V2版本
  5. 检查特定用户使用一段时间后,是否出现问题
  6. 若无问题,通过istio将所有用户的流量都指向V2版本
  7. 若所有用户都使用V2无问题,删除掉V1版本的pod

示例介绍

前端应用frontend,后端应用mqtt-server,后端应用mqtt-server 通过mqtt协议与设备相连接。
前端部署3个版本,分别是V1,V2,V3,后端同样部署3个版本,也是V1,V2,V3。3个前端版本,按钮文字不一样。3个后端版本,连接的mqtt设备不一样

版本 前端页面 后端返回参数
V1 显示V11按钮
{"message":["wsytest010","wsytest002",
"wsytest003","wsytest007","wsytest006",
"wsytest001","wsytest005","wsytest009",
"wsytest008","wsytest004"]}
V2 显示V22按钮
{"message":["wsytest019","wsytest020",
"wsytest017","wsytest012","wsytest011",
"wsytest014","wsytest018","wsytest015",
"wsytest013","wsytest016"]}
V3 显示V33按钮
{"message":["wsytest024","wsytest028",
"wsytest022","wsytest026","wsytest027",
"wsytest021","wsytest025","wsytest030",
"wsytest023","wsytest029"]}


根据需求,版本不能串,比如前端V1->后端V1,不允许出现前端V1→后端V2这样的情况发生
这里我们在选择分配流量方式时,不能使用权重的方式进行分配,只能选择指定用户或者指定IP,如果选择权重的方式,可能会出现如下的问题:
前端会访问多个js,css等文件,如果使用权重的方式,会出现一部分js来源于v1版本,一部分css来源于v2版本。
后端也同理,如果一个页面打开时,触发多个后端请求,部分来源于V2,部分来源于V1,肯定会导致前端显示出现问题。
所以只有把前后端通过某种方式一一对应,才能正常使用


代码实现与注意事项

1.部署前端的3个应用程序,所有的pod都加上 labels:[app:frontend,version:#{对应版本}]

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  labels:
    app: frontend
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
      version: v1
  template:
    metadata:
      labels:
        app: frontend
        version: v1
    spec:
      containers:
      - name: frontend
        image: 前端镜像:v1
        securityContext:
          capabilities:
            add: ["NET_ADMIN", "NET_RAW"]    # 按照istio的说明,最好把这个pod安全策略加上
        imagePullPolicy: Always
        ports:
        - containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-v2
  labels:
    app: frontend
    version: v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
      version: v2
  template:
    metadata:
      labels:
        app: frontend
        version: v2
    spec:
      containers:
      - name: frontend
        image: 前端镜像:v2
        securityContext:
          capabilities:
            add: ["NET_ADMIN", "NET_RAW"]
        imagePullPolicy: Always
        ports:
        - containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-v3
  labels:
    app: frontend
    version: v3
spec:
  replicas: 1
  selector:
    matchLabels:
      app: frontend
      version: v3
  template:
    metadata:
      labels:
        app: frontend
        version: v3
    spec:
      containers:
      - name: frontend
        image: 前端镜像:v3
        securityContext:
          capabilities:
            add: ["NET_ADMIN", "NET_RAW"]
        imagePullPolicy: Always
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
spec:
  selector:
    app: frontend
  type: ClusterIP   #这个不用NodePort,因为流量如果是从NodePort进来的,就控不住的
  ports:
    - port: 80
      targetPort: 80
      name: http-web

2.部署后端应用程序,与前端应用类似

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mqtt-server-v1
  labels:
    app: mqtt-server
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mqtt-server
      version: v1
  template:
    metadata:
      labels:
        app: mqtt-server
        version: v1
    spec:
      serviceAccountName: mqtt-server
      containers:
      - name: mqtt-server
        image: 后端镜像:latest
        securityContext:
          capabilities:
            add: ["NET_ADMIN", "NET_RAW"]    # 按照istio的说明,最好把这个pod安全策略加上
        imagePullPolicy: Always
        ports:
        - containerPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mqtt-server-v2
  labels:
    app: mqtt-server
    version: v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mqtt-server
      version: v2
  template:
    metadata:
      labels:
        app: mqtt-server
        version: v2
    spec:
      serviceAccountName: mqtt-server
      containers:
      - name: mqtt-server
        image: 后端镜像:latest
        securityContext:
          capabilities:
            add: ["NET_ADMIN", "NET_RAW"]
        imagePullPolicy: Always
        ports:
        - containerPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mqtt-server-v3
  labels:
    app: mqtt-server
    version: v3
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mqtt-server
      version: v3
  template:
    metadata:
      labels:
        app: mqtt-server
        version: v3
    spec:
      serviceAccountName: mqtt-server
      containers:
      - name: mqtt-server
        image: 后端镜像:latest
        securityContext:
          capabilities:
            add: ["NET_ADMIN", "NET_RAW"]
        imagePullPolicy: Always
        ports:
        - containerPort: 8000
---
apiVersion: v1
kind: Service
metadata:
  name: mqtt-server
spec:
  selector:
    app: mqtt-server
  type: NodePort   #这个不用NodePort,因为流量如果是从NodePort进来的,就控不住的
  ports:
    - port: 8000
      targetPort: 8000
      name: http-web

3.区分外部流量和内部流量。我们将浏览器到前端的称为外部流量,K8S里的例如前端到后端的称为内部流量

4.外部流量出去,需要被istio的ingress gateway管控起来,所以需要配置一个gateway

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: bookinfo
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"

5.配置后端的virtualservice和destination,确保后端程序能与前端程序产生一对一的关系,在无对应关系时,默认使用V1版本

---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: mqtt-server-internal
spec:
  hosts:
  - "mqtt-server"     #此处是关键,把匹配到该url的流量,全部走到这个特定的virtualservice里
  http:
  - match:
    - sourceLabels:
        version: v1
    route:
    - destination:
        host: mqtt-server
        subset: v1             # 将匹配到的流量,转向subset的v1版本,这个subset: v1在destination.yaml里定义
      headers:
        response:
          add:
            user: v1
  - match:
    - sourceLabels:
        version: v2
    route:
    - destination:
        host: mqtt-server
        subset: v2
      headers:
        response:
          add:
            user: v2
  - match:
    - sourceLabels:
        version: v3
    route:
    - destination:
        host: mqtt-server
        subset: v3
      headers:
        response:
          add:
            user: v3
  - route:
    - destination:
        host: mqtt-server
        subset: v1
      headers:
        response:
          add:
            user: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: mqtt-server
spec:
  host: mqtt-server.default.svc.cluster.local
  subsets:
  - name: v1
    labels:
      version: v1    # 根据pod的 version: v1 的label来进行匹配
  - name: v2
    labels:
      version: v2
  - name: v3
    labels:
      version: v3

6.配置前端的virtualservice和destination,我们可以设置来源于192.168.0.58这个IP的走V2版本,其余IP走V1版本

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: frontend-server
spec:
  hosts:
  - "外网域名"     #此处是关键,把匹配到该url的流量,全部走到这个特定的virtualservice里
  gateways:
  - bookinfo-gateway              #此处必须对应上gateway的名字
  http:
  - match:
    - headers:
      X-Forwarded-For:
          exact: "192.168.0.58"            #此处表示匹配header里有{"user":"v1"}
    route:
    - destination:
        host: mqtt-server
        subset: v2             # 将匹配到的流量,转向subset的v1版本,这个subset: v1在destination.yaml里定义
      headers:
        response:
          add:
            user: v2
  - route:
    - destination:
        host: frontend
        subset: v1             # 将匹配到的流量,转向subset的v1版本,这个subset: v1在destination.yaml里定义
      headers:
        response:
          add:
            user: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: frontend
spec:
  host: frontend
  subsets:
  - name: v1
    labels:
      version: v1    # 根据pod的 version: v1 的label来进行匹配
  - name: v2
    labels:
      version: v2
  - name: v3
    labels:
      version: v3

7.因为我们的浏览器访问的时候,会经过istio,所以前端收到的IP并不是真是的IP,我们需要修改istio的ingress文件,把spec.externalTrafficPolicy设置成Local,如下图所示

8.最终情况


实验效果图

1.当本机IP地址不符合条件时,前端和后端都是V1版本的结果,第一张图是实际效果,第二张图是kiali显示的流量图
2.当本机IP符合条件时,前端和后端都是V2版本的结果,左图是实际效果,右图是kiali显示的流量图

3.当同时有满足IP和不满足IP条件的机器访问时,流量图效果如下

VUE+FLASK前后端分离(回答面试题之“你来说说restful前后端代码怎么写”)

动机

前段时间出去面试,遇到了好几次对方面试官问这样的问题,”restful风格的代码,前后端怎么写?”,“从浏览器前端到后台经过了哪些?说的越详细越好。”这样的问题,我听到的第一时刻是懵逼的,啥情况?我要从vue的双向数据绑定开始说吗?axios的用法要说吗?falsk的restful是如何用‘’/api/‘’对应前台的url的吗?还是去说spring框架的mvc? 产生这样的疑惑,主要原因是,我不明白面试官为什么要问这样的问题?实现起来很简单,但是说起来又太宽泛,不知道说的是不是面试官想要的答案,容易偏题。 在我回头仔细想想了之后,决定以后再遇到这样的问题,就使用vue+falsk做例子来讲解这个。

回答策略

按照下面的几步,顺序回答

  1. 以vue+flask 前后端分离为基础,以用户登录,输入用户名密码为场景。
  2. vue前端框架通过v-model获得输入框输入的用户名以及密码。通过引入axios向后台发起http请求,axios是一个http库,可以在nodejs中使用,使用方式有一点类似ajax。通过axios.post(“/api”,{param:”param”})的方式向后台发起http请求。
  3. 后台的flask运行起来之后,通过装饰圈route.配置路由@app.route(‘/api’,methods=[“GET”,”POST”]) 来对应前台http请求的url,如果没有对应的url会返回404。如果找到对应的路由,则会进入相应的方法,进行运算,完成运算之后,可以用json.dumps把数据作为json返回。
  4. axios前台的response收到后,通过response.data获得返回的json,然后可以把相应的值进行变更

代码实现

  • 前端vue关键代码之 index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>vueapp01</title>
  <script src="https://cdn.staticfile.org/vue/2.4.2/vue.min.js"></script>
</head>
<style>
  .class1{
    background: #444;
    color: #eee;
  }
</style>
<body>
  <div id="app3"></div>
</body>
</html>
  • 前端vue关键代码之main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import Lzw from './components/Lzw'
import router from './router'
import axios from 'axios'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
Vue.use(BootstrapVue)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
  el: '#app3',
  router,
  axios,
  components: { Lzw },
  template: '<Lzw/>'
})
  • 前端vue关键代码之Lzw.vue

使用axios需要先安装axios库和HTTP2库

npm install –save axios
npm install –save http2

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <div id="example-3">
      <input type="checkbox" id="jack" value="白金会员" v-model="checkedNames">
      <label for="jack">白金会员</label>
      <input type="checkbox" id="john" value="黄金会员" v-model="checkedNames">
      <label for="john">黄金会员</label>
      <input type="checkbox" id="mike" value="王者会员" v-model="checkedNames">
      <label for="mike">王者会员</label>
      <br>
      <strong>选择会员种类: {{ checkedNames }}</strong>
    </div>
    <div id="app2">
      <input v-model="username" placeholder="用户名">
      <p>用户名是: {{ username }}</p>
      <input type="password" v-model="password" placeholder="密码">
      <p>密码是: {{ password }}</p>
      <button class="btn btn-large btn-primary" v-on:click="login">向后台发送post请求,传递用户名和密码,变更用户ID</button>
      <p>用户ID是: {{ id }}</p>
      <button class="btn btn-large btn-primary" v-on:click="getmsg">向后台发送get请求,把用户ID变成0</button>
    </div>
  </div>
</template>
<script>
import axios from "axios";
export default {
  name: "hello",
  data() {
    return {
      msg: "欢迎来到测试开发笔记!",
      checkedNames: [],
      username: "",
      password: "",
      id: "密码反转+用户名反转"
    };
  },
  methods: {
    login() {
      var that = this;
      // 对应 Python 提供的接口,这里的地址填写下面服务器运行的地址,本地则为127.0.0.1,外网则为 your_ip_address
      const path = "http://127.0.0.1:5000/getMsg";
      axios
        .post(path, { username: this.username, password: this.password })
        .then(response => {
          this.id = response.data.userid;
        });
    },
    getmsg() {
      var that = this;
      // 对应 Python 提供的接口,这里的地址填写下面服务器运行的地址,本地则为127.0.0.1,外网则为 your_ip_address
      const path = "http://127.0.0.1:5000/getMsg";
      // 务必使用箭头函数的方法,这样this.id能直接对上,不然会报错提示id没找到
      axios
        .get(path, { username: this.username, password: this.password })
        .then(response => {
          this.id = response.data.userid;
        });
    }
  }
};
</script>
  • 后端flask关键代码main.py

flask要避免跨域问题。需要安装Flask库和Falsk-Cors库

from flask import Flask, url_for,request
from flask_cors import *
import json
app = Flask(__name__)
# 这句话解决跨域问题
CORS(app, supports_credentials=True)
@app.route('/getMsg',methods=["GET","POST"])
def getMsg():
    if request.method == 'POST':
        username = request.json['username']
        password= request.json['password']
        # 假定用户id是密码反转+用户名反转得出来的
        datat = {
            "userid": username[::-1]+password[::-1],
        }
        return json.dumps(datat)
    elif request.method == 'GET':
        datat = {
            "userid": 0,
        }
        return json.dumps(datat)
if __name__ == '__main__':
    app.debug = True
    app.run()

vue框架html中的script内容转换成.vue文件的script内容

问题:
1.因为前段时间学习vue,都是在一个index.html中添加<html>和<script>
2.如果使用.vue文件,<template>对应index.html<html>作为展示层,<script>作为逻辑层对应index.html的<script>
html中用的是new vue。而.vue文件中用的是export default。下面是对比

  • index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>vueapp01</title>
  <script src="https://cdn.staticfile.org/vue/2.4.2/vue.min.js"></script>
</head>
<style>
  .class1{
    background: #444;
    color: #eee;
  }
</style>
<body>
  <div id="app"></div>
  <div id='example-3'>
    <input type="checkbox" id="jack" value="白金会员" v-model="checkedNames">
    <label for="jack">白金会员</label>
    <input type="checkbox" id="john" value="黄金会员" v-model="checkedNames">
    <label for="john">黄金会员</label>
    <input type="checkbox" id="mike" value="王者会员" v-model="checkedNames">
    <label for="mike">王者会员</label>
    <br>
    <span>选择会员种类: {{ checkedNames }}</span>
  </div>
  <div id="app2">
    <input v-model="username" placeholder="用户名">
    <p>用户名是: {{ username }}</p>
    <input  type="password" v-model="password" placeholder="密码">
    <p>密码是: {{ password }}</p>
    <button v-on:click="login">登录</button>
  </div>
</body>
<script type="text/javascript">
  var chenames=new Vue({
    el: '#example-3',
    data: {
      checkedNames: []
    }
  })
  var user=new Vue({
    el :'#app2',
    data: {
      username:'',
      password:''
    },
    methods :{
      login:function(event){
        alert('用户名是:'+this.username+',密码是:'+this.password+',选择的是:'+chenames.checkedNames)
      }
    }
  })
</script>
</html>
  • .vue文件
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <div id='example-3'>
    <input type="checkbox" id="jack" value="白金会员" v-model="checkedNames">
    <label for="jack">白金会员</label>
    <input type="checkbox" id="john" value="黄金会员" v-model="checkedNames">
    <label for="john">黄金会员</label>
    <input type="checkbox" id="mike" value="王者会员" v-model="checkedNames">
    <label for="mike">王者会员</label>
    <br>
    <span>选择会员种类: {{ checkedNames }}</span>
  </div>
  <div id="app2">
    <input v-model="username" placeholder="用户名">
    <p>用户名是: {{ username }}</p>
    <input  type="password" v-model="password" placeholder="密码">
    <p>密码是: {{ password }}</p>
    <button class="btn btn-large btn-primary" v-on:click="login">登录</button>
  </div>
  </div>
</template>
<script>
export default {
  name: "hello",
  data() {
    return {
      msg: "欢迎来到测试开发笔记!",
      checkedNames:[],
      username: "",
      password: "",
    };
  },
  methods:{
    login(){
       alert('用户名是:'+this.username+',密码是:'+this.password+',选择的是:'+this.checkedNames)
    }
  }
};
</script>

如何使用Vue获得多个input和多选框的值,以及双向绑定

需求分析

1.有三个多选框选项。当选择不同的按钮时,界面上会实时展示所选的参数
2.有用户名和密码,当输入用户名和密码时,界面上会实时展示所输入的参数
3.当点击登录按钮的时候,会弹出alert,展示当前所选的所有参数

实现代码

通过v-model实时获得input的输入值。通过v-on监听login事件。完整代码如下

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>vueapp01</title>
  <script src="https://cdn.staticfile.org/vue/2.4.2/vue.min.js"></script>
</head>
<style>
  .class1{
    background: #444;
    color: #eee;
  }
</style>
<body>
  <!-- <div id="app"></div> -->
  <div id='example-3'>
    <input type="checkbox" id="jack" value="白金会员" v-model="checkedNames">
    <label for="jack">白金会员</label>
    <input type="checkbox" id="john" value="黄金会员" v-model="checkedNames">
    <label for="john">黄金会员</label>
    <input type="checkbox" id="mike" value="王者会员" v-model="checkedNames">
    <label for="mike">王者会员</label>
    <br>
    <span>选择会员种类: {{ checkedNames }}</span>
  </div>
  <div id="app2">
    <input v-model="username" placeholder="用户名">
    <p>用户名是: {{ username }}</p>
    <input  type="password" v-model="password" placeholder="密码">
    <p>密码是: {{ password }}</p>
    <button v-on:click="login">登录</button>
  </div>
</body>
<script type="text/javascript">
  var chenames=new Vue({
    el: '#example-3',
    data: {
      checkedNames: []
    }
  })
  var user=new Vue({
    el :'#app2',
    data: {
      username:'',
      password:''
    },
    methods :{
      login:function(event){
        alert('用户名是:'+this.username+',密码是:'+this.password+',选择的是:'+chenames.checkedNames)
      }
    }
  })
</script>
</html>

electron实例(一个例子教你入门)

总览

electron是目前最流行的桌面级开发框架。作为一个老测开,不能停下学习的脚本。下面我用一个electron制作了一个登陆的例子,用到的功能包含简单的ipc通信,页面跳转,引入jquery,bootstrap。下面开始我们的分解动作。

文件目录


该工程是我下载了github上的quick-start的工程,自己修改了main.js和index.html。添加了一个web-content/welcome.html页面。

  • main.js是后端主进程文件
  • index.html是首页
  • welcome.html是登录成功之后的页面

main.js(主进程)

main.js也就是主进程。这里我贴出用处最大的ipc前后端通信的代码。
讲解:后端用的是ipcMain。前端用的是ipcRender,这个开发模式是发布/订阅模式。ipc.on()发布一个‘login’的监听消息。当前端发起‘login’的请求时,触发后台的funtion()。mainWindow控制页面跳转。mainWindow.loadFile(‘./web-content/welcome.html’)用当前窗口打开一个welcome页面。

let mainWindow
const ipc = require('electron').ipcMain
ipc.on('login', function(event, user_name,user_password) {
  result='登录失败';
  console.log(user_name,user_password);  // 打印 "用户名密码"
  if(user_name=="1" && user_password=="2"){
     result='登录成功';
     console.log(result);
     //登录成功之后打开welcome页面
     mainWindow.loadFile('./web-content/welcome.html')
  }else{
    console.log(result);
  }
  //因为是同步策略,所以用event.returnValue
  event.returnValue = result;
  console.log("end");
});

index.html(首页)


首页效果如上图,用户名输入1,密码输入2,点击submit按钮开始登陆。
为了引用bootstrap,我们在最前面的<head>下加入bootstrap和jquery

<head>
    <title>测试开发笔记</title>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
    <script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
    <script>if (window.module) module = window.module;</script>
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
</head>
ipcRenderer连接后端,通过sendSync来同步发送消息,发送用户名和密码到后端,如果为真,新窗口打开github首页。用了jquery的$定位元素。用了.val()获得元素里填写的值,.afrer()在指定位置后添加元素。
<script>
  function connectMain() {
    const ipcRenderer = require('electron').ipcRenderer;
    var user_name=$("#input_username").val()
    var user_password=$("#input_userpassword").val()
    result=ipcRenderer.sendSync('login',user_name,user_password)
    if(result=="登录成功"){
      const remote = require('electron').remote;
      const BrowserWindow = remote.BrowserWindow;
      var win = new BrowserWindow({ width: 800, height: 600 });
      win.loadURL('https://github.com');
      // BrowserWindow.loadURL('https://github.com');
    }
    else{
      console.log($("#input_username"))
      var txt1="<div>密码错误</div>";
      $("#input_userpassword").after(txt1)
    }
    console.log(result)
  }
</script>

完整代码

main.js
// Modules to control application life and create native browser window
const {app, BrowserWindow,Menu,shell} = require('electron')
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow
const ipc = require('electron').ipcMain
ipc.on('login', function(event, user_name,user_password) {
  result='登录失败';
  console.log(user_name,user_password);  // 打印 "用户名密码"
  if(user_name=="1" && user_password=="2"){
     result='登录成功';
     console.log(result);
     //登录成功之后打开welcome页面
     mainWindow.loadFile('./web-content/welcome.html')
  }else{
    console.log(result);
  }
  //因为是同步策略,所以用event.returnValue
  event.returnValue = result;
  console.log("end");
});
function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 800, height: 600})
  // and load the index.html of the app.
  mainWindow.loadFile('index.html')
  // Open the DevTools.
  // mainWindow.webContents.openDevTools()
  // Emitted when the window is closed.
  mainWindow.on('closed', function () {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null
  })
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
  createWindow()
})
// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On OS X it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})
app.on('activate', function () {
  // On OS X it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow()
  }
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
index.html
<!DOCTYPE html>
<html>
<head>
    <title>测试开发笔记</title>
    <meta charset="utf-8">
    <link rel="stylesheet" type="text/css" href="node_modules/bootstrap/dist/css/bootstrap.min.css">
    <script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
    <script>if (window.module) module = window.module;</script>
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/bootstrap/4.1.3/js/bootstrap.min.js"></script>
</head>
<body>
  <div class="container-fluid">
    <h1 class="page-header">electron-bootstrap实例</h1>
    <div class="row">
      <div class="col-md-offset-8 col-sm-offset-3 col-md-4 col-sm-6">
        <div class="panel panel-default">
          <div class="panel-body">
            <form>
              <div class="form-group">
                <label for="input_username">用户名</label>
                <input type="email" class="form-control" id="input_username" placeholder="Email">
              </div>
              <div class="form-group">
                <label for="input_userpassword">密码</label>
                <input type="password" class="form-control" id="input_userpassword" placeholder="Password">
              </div>
              <div class="checkbox">
                <label>
                  <input type="checkbox"> 记住密码
                </label>
              </div>
              <button type="submit" id="abc" class="btn btn-default" onclick="connectMain()">开始登陆</button>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
<script>
  function connectMain() {
    const ipcRenderer = require('electron').ipcRenderer;
    var user_name=$("#input_username").val()
    var user_password=$("#input_userpassword").val()
    result=ipcRenderer.sendSync('login',user_name,user_password)
    if(result=="登录成功"){
      const remote = require('electron').remote;
      const BrowserWindow = remote.BrowserWindow;
      var win = new BrowserWindow({ width: 800, height: 600 });
      win.loadURL('https://github.com');
      // BrowserWindow.loadURL('https://github.com');
    }
    else{
      console.log($("#input_username"))
      var txt1="<div>密码错误</div>";
      $("#input_userpassword").after(txt1)
    }
    console.log(result)
  }
</script>
</body>
    <script src="./renderer.js"></script>
</html>
welcome.html
<!DOCTYPE html>
<html>
<head>
    <title>es2105的写法</title>
    <meta charset="utf-8">
</head>
<body>
hello,world
</body>
</html>
renderer.js
function connectMain() {
    const ipcRenderer = require('electron').ipcRenderer;
    var user_name=$("#input_username").val()
    var user_password=$("#input_userpassword").val()
    result=ipcRenderer.sendSync('login',user_name,user_password)
    if(result=="登录成功"){
      const remote = require('electron').remote;
      const BrowserWindow = remote.BrowserWindow;
      var win = new BrowserWindow({ width: 800, height: 600 });
      win.loadURL('https://github.com');
      // BrowserWindow.loadURL('https://github.com');
    }
    else{
      console.log($("#input_username"))
      var txt1="<div>密码错误</div>";
      $("#input_userpassword").after(txt1)
    }
    console.log(result)
  }
package.json
{
  "name": "electron-quick-start",
  "version": "1.0.0",
  "description": "A minimal Electron application",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "repository": "https://github.com/electron/electron-quick-start",
  "keywords": [
    "Electron",
    "quick",
    "start",
    "tutorial",
    "demo"
  ],
  "author": "GitHub",
  "license": "CC0-1.0",
  "devDependencies": {
    "electron": "^2.0.0"
  },
  "dependencies": {
    "bootstrap": "^4.1.3",
    "electron-json-storage": "^4.1.4",
    "jquery": "^1.12.4",
    "popper.js": "^1.14.4"
  }
}

参考

https://wizardforcel.gitbooks.io/electron-doc/content/api/ipc-main.html
https://electronjs.org/docs/tutorial/first-app