最近在着手实现 Console k8s workload upgrade 相关的任务,然而 mock 数据不直观,使用完整的集成环境,又存在不稳定性,经常与其他任务冲突(比如测试要开始搞破坏了)。遂重新开始折腾 k8s cluster setup

当然 k8s 发展到今天 local up 已经做的相对简单(可能对于自由的网络环境来说,不自由的就求爷爷告奶奶了)

这里 local up k8s cluster 使用的是 minikube 方案 https://v1-9.docs.kubernetes.io/docs/getting-started-guides/minikube/

此篇适用下述两条背景

  • 非阿里 minikube 版本
  • 解决 minikube docker proxy not work 问题

Environment

  • OS: macOS Sierra 10.12.6
  • minikube: v0.25.0
  • VM Driver: virtualbox
  • ISO version: minikube-v0.25.1.iso

Install

macOS 安装 minikube 可以说很简单了

已安装 homebrew

homebrew install minikube

1
brew cask install minikube

Network

国内的困难主要是这个

我使用的方案是 Shadowsocks + privoxy,相关的资料非常多,不赘述了

privoxy 的配置如下 cat /usr/local/etc/privoxy/config

1
2
listen-address 0.0.0.0:1081
forward-socks5 / 127.0.0.1:1080 .

1080 端口为 socks5 的监听端口(即 Shadowsocks),privoxy 监听 1081 端口,将 1081 端口的 http 请求转发至 1080 socks5 监听端口上,这样就能让 http 请求也经过 socks5 转发了

测试一下 privoxy 是否正常工作

1
2
3
4
5
6
7
8
# check process
ps -ef | grep privoxy
# curl port
curl http://127.0.0.0:1081
# netstat (recommended)
netstat -an | grep 1081
# lsof
lsof -i:1081

Minikube start

使用上一步搭建好的 privoxy http 代理

我使用的终端为 iTerm,理论上系统自带 Terminal 也 ok

设置 http / https 代理

1
2
export http_proxy=http://127.0.0.1:1081
export https_proxy=https://127.0.0.1:1081

启动 minikube

1
minikube start

Docker proxy

minikube 下载完成 iso 后,再 bootstrap k8s cluster,cluster 起来之后,会启动一些 system 组件,比如 kube-addon-manager-minikube/ dashboard / dns 等,然而不幸的是这些 pod 会一直处于 ContainerCreating 的状态

查看 events

1
kubectl describe po kube-addon-manager-minikube -nkube-system

None

查看 kubelet 日志

1
minikube logs kubelet

发现端倪

gcr.io/google_containers/pause-amd64:3.0,pause 容器无法 pull 下来

继续搜索得知 minikube 可配置 docker proxy https://github.com/kubernetes/minikube/blob/v0.25.0/docs/http_proxy.md

遂停止 minikube 并以新参数启动之

1
2
minikube start --docker-env HTTP_PROXY=http://127.0.0.1:1081 --docker-env HTTPS_PROXY=https://127.0.0.1:1081
export no_proxy=$no_proxy,$(minikube ip)

呃,然而事实证明并不 work,遂登陆 vm

1
minikube ssh

尝试 curl 该 1081 端口

1
2
curl http://127.0.0.1:1081
>> curl: (7) Failed to connect to 127.0.0.1 port 1081: Connection refused

可见 vm 中该端口并不通

稍加思索,解决的思路应为在 virtualbox vm 中如何 connect back to host,因为 privoxy 实际上监听的是 host 的 1081 端口。几番搜索后,发现在 virtualbox vm 中可通过 10.0.2.2 IP connect back to host https://superuser.com/questions/310697/connect-to-the-host-machine-from-a-virtualbox-guest-os,登陆 vm

1
curl http://10.0.2.2:1081

果然能通了

于是再次尝试修改 minikube 启动命令

1
minikube start --docker-env HTTP_PROXY=http://10.0.2.2:1081 --docker-env HTTPS_PROXY=https://10.0.2.2:1081

糟糕的是,仍然不 work …,所以问题集中到了 http_proxy 未生效上

登陆 vm 查看 docker version / info,希望能获取到一些线索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# version
docker version
Client:
Version: 17.09.0-ce
API version: 1.32
Go version: go1.8.3
Git commit: afdb6d4
Built: Tue Sep 26 22:39:28 2017
OS/Arch: linux/amd64
Server:
Version: 17.09.0-ce
API version: 1.32 (minimum version 1.12)
Go version: go1.8.3
Git commit: afdb6d4
Built: Tue Sep 26 22:45:38 2017
OS/Arch: linux/amd64
Experimental: false
# info
docker info
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 17.09.0-ce
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
Volume: local
Network: bridge host macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 06b9cb35161009dcb7123345749fef02f7cea8e0
runc version: 3f2f8b84a77f73d38244dd690525642a72156c64
init version: N/A (expected: )
Security Options:
seccomp
Profile: default
Kernel Version: 4.9.64
Operating System: Buildroot 2017.11
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 1.953GiB
Name: minikube
ID: 6RR3:WAF4:FIGA:TTEG:5UE6:V3RD:JNQV:WQQ4:ER3T:ETKJ:ZVP4:2Z7M
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
provider=virtualbox
Experimental: false
Insecure Registries:
10.96.0.0/12
127.0.0.0/8
Live Restore Enabled: false

然而似乎和问题并没有什么关联,http_proxy 和 docker daemon 有关,和 docker 没啥关系,所以在 version / info 中都未见 http_proxy 相关配置 https://github.com/docker/distribution/issues/2397#issuecomment-330079118

追溯到这里,只能看看 minikube 代码中是如何使用 docker-env 这个传入参数的了

how does minikube start

overall

  • startHost
  • startK8S

startHost

  • create virtualbox driver with boot2docker iso
  • waiting docker set up

step by step 的过程省略,最后找到如下代码片段,显示 docker-env 实际上并不会生效

https://github.com/kubernetes/minikube/blob/v0.25.0/cmd/minikube/cmd/start.go#L157

1
2
3
4
5
6
7
start := func() (err error) {
host, err = cluster.StartHost(api, config)
if err != nil {
glog.Errorf("Error starting host: %s.\n\n Retrying.\n", err)
}
return err
}

https://github.com/kubernetes/minikube/blob/v0.25.0/pkg/minikube/machine/client.go#L114-L135

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
return &host.Host{
ConfigVersion: version.ConfigVersion,
Name: driver.GetMachineName(),
Driver: driver,
DriverName: driver.DriverName(),
HostOptions: &host.Options{
AuthOptions: &auth.Options{
CertDir: api.certsDir,
CaCertPath: filepath.Join(api.certsDir, "ca.pem"),
CaPrivateKeyPath: filepath.Join(api.certsDir, "ca-key.pem"),
ClientCertPath: filepath.Join(api.certsDir, "cert.pem"),
ClientKeyPath: filepath.Join(api.certsDir, "key.pem"),
ServerCertPath: filepath.Join(api.GetMachinesDir(), "server.pem"),
ServerKeyPath: filepath.Join(api.GetMachinesDir(), "server-key.pem"),
},
EngineOptions: &engine.Options{
StorageDriver: "aufs",
TLSVerify: true,
},
SwarmOptions: &swarm.Options{},
},
}, nil

可见默认 SwarmOptions 是个空对象,其中值得注意的是 IsSwarm 的值为 false

https://github.com/kubernetes/minikube/blob/v0.25.0/pkg/minikube/cluster/cluster.go#L243-L250

1
2
3
4
5
6
7
h, err := api.NewHost(config.VMDriver, data)
if err != nil {
return nil, errors.Wrap(err, "Error creating new host")
}
h.HostOptions.AuthOptions.CertDir = constants.GetMinipath()
h.HostOptions.AuthOptions.StorePath = constants.GetMinipath()
h.HostOptions.EngineOptions = engineOptions(config)

将 docker-env 赋值与 h.HostOptions.EngineOptions.Env

https://github.com/kubernetes/minikube/blob/v0.25.0/vendor/github.com/docker/machine/libmachine/provision/boot2docker.go#L232

1
2
3
4
provisioner.SwarmOptions = swarmOptions
provisioner.AuthOptions = authOptions
provisioner.EngineOptions = engineOptions
swarmOptions.Env = engineOptions.Env

最后将 docker-env 传与 swarmOptions.Env,而我们又知道 IsSwarm 的值为 false,因此实际上该配置并不会生效 … 社区真是给处于不自由网络地区的童鞋埋了个大坑 …

Back to Docker proxy

回到如何在 minikube 中配置 Docker proxy 的问题,实际上可以参考 Docker 官方的文档配置,使用 systemd

https://docs.docker.com/config/daemon/systemd/#httphttps-proxy

1
2
3
minikube ssh
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo vi /etc/systemd/system/docker.service.d/http-proxy.conf

输入如下内容并保存退出

1
2
[Service]
Environment="HTTP_PROXY=http://10.0.2.2:1081" "HTTPS_PROXY=https://10.0.2.2:1081"

Flush changes & Restart Docker

1
2
sudo systemctl daemon-reload
sudo systemctl restart docker

Verify that the configuration has been loaded

1
systemctl show --property=Environment docker

执行结束之后,此时在 vm 中

1
docker pull gcr.io/google_containers/pause-amd64:3.0

终于可以正常 pull image 了,system pod 也可以正常 running 起来了

1
2
3
4
5
NAME                                    READY     STATUS    RESTARTS   AGE
kube-addon-manager-minikube 1/1 Running 4 12h
kube-dns-54cccfbdf8-wfnxm 3/3 Running 3 12h
kubernetes-dashboard-77d8b98585-t59gj 1/1 Running 1 12h
storage-provisioner 1/1 Running 1 12h

不过因为该改动并未固化到 iso 中,因此 minikube stop 之后改动会丢失 … 另外一个折中的办法

Another method for Docker proxy (recommended)

workaround

之前我们知道,在 terminal 中 export http_proxy 之后,minikube 即可使用 proxy 访问网络资源,而在 minikube –help 中发现 minikube 可以 cache image,所以我们可以 cache 需要使用的 image 资源,如

1
2
3
export http_proxy=http://127.0.0.1:1081
export no_proxy=$no_proxy,$(minikube ip)
minikube cache add gcr.io/google_containers/pause-amd64:3.0

也可以解决问题,比如我目前 cache 的 image

1
2
3
4
5
6
7
8
minikube cache list
gcr.io/google-containers/kube-addon-manager:v6.5
gcr.io/google_containers/pause-amd64:3.0
gcr.io/k8s-minikube/storage-provisioner:v1.8.1
k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.14.5
k8s.gcr.io/k8s-dns-kube-dns-amd64:1.14.5
k8s.gcr.io/k8s-dns-sidecar-amd64:1.14.5
k8s.gcr.io/kubernetes-dashboard-amd64:v1.8.1

cache 这些 image 之后,就可以使得 kube-system 下面的 pod 都 running 了

minikube logs healthcheck error

使用 minikube 过程中发现其 logs 中一直有如下错误日志

1
Jun 23 18:15:15 minikube localkube[3034]: E0623 18:15:15.392453    3034 healthcheck.go:317] Failed to start node healthz on 0: listen tcp: address 0: missing port in address

查看了相关代码,似乎是正常现象,不过这个实现也是太奇怪了 … 可参考如下 issue,https://github.com/kubernetes/minikube/issues/2609#issuecomment-399701288

k8s 1.7.6

kubernetes/pkg/controller/volume/persistentvolume/pv_controller.go

claim.Spec.VolumeName == nil 时

storage-controller 会从已有的 volume 中查找符合该 claim 的 volume,如果没找到该 volume,则从 claim 的 annotation 中获取 volume.beta.kubernetes.io/storage-class 字段 / 或从 Spec.StorageClassName 获取 storage-class 的值

  • volume.beta.kubernetes.io/storage-class
  • Spec.StorageClassName

在允许动态提供存储(enableDynamicProvisioning)的情况下,尝试去提供一个 volume

1
newClaim, err := ctrl.setClaimProvisioner(claim, storageClass)

动态的 pvc 会增加如下 annotation

1
2
3
volume.beta.kubernetes.io/storage-provisioner: class.Provisioner
pv.kubernetes.io/bound-by-controller: yes
pv.kubernetes.io/provisioned-by: plugin.GetPluginName()

从存储提供服务获取 volume 的过程是异步的,当获取完成时,设置如下 annotation

1
pv.kubernetes.io/bind-completed: yes

如果是其他可以直接 bind 的情况,在 bind 的方法中也会设置上述 annotation

所以可以通过 pvc annotation

  • pv.kubernetes.io/bound-by-controller: yes

确认该 pvc 为动态创建还是直接使用

不过该字段还有一种情况下可能被设置

1
2
3
4
5
6
7
8
9
if volume.Spec.ClaimRef == nil {
return false
}
if claim.Name != volume.Spec.ClaimRef.Name || claim.Namespace != volume.Spec.ClaimRef.Namespace {
return false
}
if volume.Spec.ClaimRef.UID != "" && claim.UID != volume.Spec.ClaimRef.UID {
return false
}

volume 和 claim binding 时,发现 volume 与 claim 的字段不匹配

1
2
3
4
5
// Check if the claim was already bound (either by controller or by user)
shouldBind := false
if volume.Name != claim.Spec.VolumeName {
shouldBind = true
}

clain 和 volume binding 时,也会出现这种情况,当 volume 与 claim 的字段不匹配时

目前实践经验不足,还不是特别明白这是什么情况下才会出现的,使用 pv.kubernetes.io/bound-by-controller: yes 判断动态创建是否准确?

我们知道在 k8s 集群中,可以通过 kubeclt exec -ti 命令远程登录容器,以执行命令。而要在 Console 上实现这个特性的集成,需要依赖 websocket 协议 (https://tools.ietf.org/html/rfc6455)[https://tools.ietf.org/html/rfc6455]

下面全面回顾一下集成过程中涉及到的方方面面知识

  • kube-apiserver
  • nginx
  • tomcat
  • webpack-dev-server

exec in kube-apiserver

to be cont.

404 nginx

Problem: websocket 404 through nginx

遇到的问题,本地开发完成之后,部署到环境中,websocket 请求在直接用 IP 访问时 okay,而经过了二级域名则不 okay。二级域名是由 Nginx 配置转发的。查看 Nginx 的配置

Nginx 配置为通用的配置,即 upstream / server 的配置

1
2
3
4
5
6
7
8
9
10
upstream console {
ip_hash;
server IP:PORT;
server IP:PORT;
}
server {
location /serviceName {
proxy_pass https://console;
}
}

http 模块有如下配置

1
2
3
4
5
6
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
}

参考该文档 http://nginx.org/en/docs/http/ngx_http_map_module.html 解读 map 的语义

上述语句的语义为,动态设置 $connection_upgrade 变量的值,当请求头中 upgrade 值为 ‘’ 时,则 $connection_upgrade 值为 close,当 upgrade 值非空时,其值为 upgrade

对于 websocket 请求来说,其第一个请求中会携带请求头

1
2
Connection: upgrade
Upgrade: websocket

根据 http://nginx.org/en/docs/http/websocket.html 文档说明

As noted above, hop-by-hop headers including “Upgrade” and “Connection” are not passed from a client to proxied server

Upgrade / Connection header 并不会由 client 传递至被代理的 server,因此在 nginx 的 server 配置处需要手动增加这两 header

1
2
3
4
5
6
7
server {
location /serviceName/ws {
proxy_pass https://console;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}

之前 nginx 未这么配置,导致 ws 请求被转发至后端时,缺少了这两请求头,导致 tomcat wsservlet 没识别到这是 ws 请求,又没有其他的 filter 或 servlet 处理,因此返回了 404

websocket in tomcat

tomcat 7

上述问题中 nginx 为公共组件,需要更改配置的话,要走流程,为了可以先行调试,所以考虑在 webserver 侧想想办法解决请求头的问题

分析

在 WsFilter 中判断请求为 ws 请求的要素

  • GET 请求
  • header 中包含 Upgrade: websocket 值
1
2
3
4
5
6
7
8
9
public static boolean isWebSocketUpgradeRequest(ServletRequest request,
ServletResponse response) {
return ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse) &&
headerContainsToken((HttpServletRequest) request,
Constants.UPGRADE_HEADER_NAME,
Constants.UPGRADE_HEADER_VALUE) &&
"GET".equals(((HttpServletRequest) request).getMethod()));
}

在 WsServerContainer 中将该 Filter 加入到 servletContext 中,并设置其拦截所有请求

1
fr.addMappingForUrlPatterns(types, true, "/*");

而这个地方的调用在 WsServerContainer 构造函数中发生,继续探寻而上

WsSci 实现了 ServletContainerInitializer 接口,在 onStartup 接口实现中构造了 WsServerContainer 类

https://tomcat.apache.org/tomcat-8.0-doc/servletapi/javax/servlet/ServletContainerInitializer.html

回忆在 Tomcat 的 release 版本中 tomcat7-websocket.jar 被放置于 {CATALINA_BASE}/lib 目录下

tomcat 启动后会加载

能否实现一个 filter,该 filter 拦截 ws 请求 url,对这个 url 的请求增加特定请求头?

这么实现的话,得确认 filter 的执行顺序。我们知道

First, the matching filter mappings in the same order that these elements appear in the deployment descriptor.

Next, the matching filter mappings in the same order that these elements appear in the deployment descriptor.

那么非定义在描述符中的 filter 呢?

to be cont.

解决方案

实验时发现可行,所以可以认为描述符中的 filter 先于 WsFilter 执行

参考

https://stackoverflow.com/questions/17086712/servlet-filters-order-of-execution

神奇的 http-proxy-middleware

Problem: websocket hangs on pending status

遇到的问题,本地开发调试时,websocket 一直处于 pending 状态。查看 proxy 的 debug 信息,一直没有 get 请求发送,网上漫天搜索 issue / stackoverflow 无果,遂强行不懂装懂看代码

当然有 issue 是直接关联的,因一开始完全没经验,并未注意到实际上就是该 issue 导致的 ^_^

分析

团队开发 Console 使用的 dev tool 为

  • webpakc-dev-server 2.2.0 (WDS)

本地开发调试依赖 WDS,该组件集成了 proxy 的功能,可以将本地的请求转发至实际的后端。例如如下的 proxy 配置

1
2
3
4
5
6
7
8
9
10
11
devServer: {
port: 8888,
proxy: {
"/api": {
target: "http://127.0.0.1:50545",
changeOrigin: true,
secure: false,
logLevel: "debug"
},
}
}

按上述配置,本地的 http://localhost:8888/api… 请求会被转发至 http://127.0.0.1:50545/api...。当然在实际开发调试过程中,被转发至的地址一般为后台 API 接口地址,或者是后台代理服务器地址,这样也就实现了本地 Console 开发与后端分离

websocket 协议的 proxy 需要打开 ws option,参考如下配置

1
2
3
4
5
6
7
8
9
10
11
12
devServer: {
port: 8888,
proxy: {
"/api": {
target: "http://127.0.0.1:50545",
changeOrigin: true,
secure: false,
logLevel: "debug",
ws: true // proxy websocket
}
}
}

WDS 在启动时,会使用 express https://expressjs.com/en/4x/api.html 启动一个 web-server

express 是一个 web framework,类似 java 里的 struts,粗略来看可以定义路由被哪个 middleware 处理,以及处理逻辑

继续回到 WDS 启动时,如果 WDS 定义了 proxy 配置,则监听所有路由,将路由的处理逻辑交给 proxyMiddleware 负责

[https://github.com/webpack/webpack-dev-server/blob/v2.2.0/lib/Server.js#L196-L228)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
options.proxy.forEach(function(proxyConfigOrCallback) {
let proxyConfig;
let proxyMiddleware;
if(typeof proxyConfigOrCallback === "function") {
proxyConfig = proxyConfigOrCallback();
} else {
proxyConfig = proxyConfigOrCallback;
}
proxyMiddleware = getProxyMiddleware(proxyConfig);
app.use(function(req, res, next) {
if(typeof proxyConfigOrCallback === "function") {
const newProxyConfig = proxyConfigOrCallback();
if(newProxyConfig !== proxyConfig) {
proxyConfig = newProxyConfig;
proxyMiddleware = getProxyMiddleware(proxyConfig);
}
}
const bypass = typeof proxyConfig.bypass === "function";
const bypassUrl = bypass && proxyConfig.bypass(req, res, proxyConfig) || false;
if(bypassUrl) {
req.url = bypassUrl;
next();
} else if(proxyMiddleware) {
// proxy request at here
return proxyMiddleware(req, res, next);
} else {
next();
}
});
});

实际开发时,proxy 规则往往不仅仅配置一条,可能类似如下存在多条配置,其中第三条是 websocket api 请求的 proxy 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
devServer: {
port: 8888,
proxy: {
"/api": {
target: "http://api.company.com:50545",
changeOrigin: true,
secure: false,
logLevel: "debug",
},
"/account": {
target: "http://iam.company.com",
changeOrigin: true,
secure: false,
logLevel: "debug",
}
"/exec": {
target: "http://api.company.com:50545",
changeOrigin: true,
secure: false,
logLevel: "debug",
ws: true, // proxy websocket
}
}
}

回顾之前所说 WDS 启动时,proxy 一旦配置,会将所有路由,逐一代理给 proxyMiddleware

更具体来说对于上述例子,每一条 proxy 规则会创建一个 proxyMiddleware,而所有路由都将按 key 的字典序,逐一代理给 proxyMiddleware

对于上述 proxy 配置的处理顺序为

  1. /account proxyMiddleware
  2. /api proxyMiddleware
  3. /exec proxyMiddleware

注意 app.use 中的 next 方法,在当前 proxyMiddleware 不处理该路由后,调用 next 交由下由 middleware 继续处理,有点类似 java servlet 里的 filter

在 WDS 中这个 proxyMiddleware 是使用 http-proxy-middleware 实现的,而 http-proxy-middleware 最终依赖 http-proxy

再继续探究 ws: true option 到底干了啥事儿,使得 WDS 可以 proxy websocket 请求?

答案在 https://github.com/chimurai/http-proxy-middleware/blob/v0.17.3/lib/index.js#L38-L50

可以看到这段代码,如果 proxy 中 ws: true,那么创建该 proxyMiddleware 时会调用 catchUpgradeRequest 方法

1
2
3
4
5
6
7
8
function catchUpgradeRequest(server) {
// subscribe once; don't subscribe on every request...
// https://github.com/chimurai/http-proxy-middleware/issues/113
if (!wsInitialized) {
server.on('upgrade', wsUpgradeDebounced);
wsInitialized = true;
}
}

在 catchUpgradeRequest 方法中,使用 server 对象监听 upgrade 事件,而 wsUpgradeDebounced 调用也很简单

debounce 直译为节流:即持续操作时,不会触发,停止一段时间后才触发。多用于用户连续输入时,停止一段时间后的回调

即使用 underscore 的 debounce 方法调用 handleUpgrade

1
2
3
4
5
6
7
8
9
function handleUpgrade(req, socket, head) {
// set to initialized when used externally
wsInitialized = true;
if (shouldProxy(config.context, req)) {
var activeProxyOptions = prepareProxyRequest(req);
proxy.ws(req, socket, head, activeProxyOptions);
logger.info('[HPM] Upgrading to WebSocket');
}
}

ok,看到这,基本的逻辑都明白了,全流程走一遍

原因

按 websocket 协议来说,第一个请求为 connect upgrade 的请求,即为 http get 请求 (当然与一般的 http get 请求不同,实际上并不能等同认为是一个 http get 请求),应能在 proxy debug 信息中看到这个 get 请求,而该 debug 信息是在 https://github.com/chimurai/http-proxy-middleware/blob/v0.17.3/lib/index.js#L40https://github.com/chimurai/http-proxy-middleware/blob/v0.17.3/lib/index.js#L66 处被打印,L40 这处的打印非 websocket 请求,L66为 websocket 请求

L66 之所以未被打印,是因为未进入 catchUpgradeRequest 方法,而未进入该方法的原因,是因为在配置多条 proxy 规则时,如果按字典序来看,例子中的 /exec 排在最后,而普通 http 请求已被其他 proxyMiddleware 处理,那么就不会调用 next 方法交由下一个 proxyMiddleware 处理,因此 /exec 只有在发起 websocket 请求时才会经过

而如果 https://github.com/chimurai/http-proxy-middleware/blob/v0.17.3/lib/index.js#L56 未被执行,即 http-server 若未监听 upgrade 请求,则 websocket 的 upgrade 请求一直不会被处理,因此出现了 pending 中的状态

另外 app.use 无法拦截到 connect upgrade 请求

所以需要一个 http request warm up websocket proxy https://github.com/chimurai/http-proxy-middleware/issues/207

解决方案

对于例子中的 proxy 来说,浏览器中输入一个 404 未被任一 proxyMiddleware 处理的路由即可,比如 http://localhost:8888/warmup,这样这个请求会经过所有 proxyMiddleware,在经过 websocket proxy 时触发 server listen on upgrade。后续 websocket 发起 connect 请求时,proxy debug 日志中就能看到 websocket http get 的输出,并且有 [HPM] Upgrading to WebSocket 的输出,websocket 本地开发时就能正常连接了

参考

https://github.com/expressjs/express/issues/2594

https://github.com/chimurai/http-proxy-middleware/issues/143

https://github.com/chimurai/http-proxy-middleware/issues/112

https://github.com/chimurai/http-proxy-middleware/issues/207

https://github.com/kubernetes-ui/container-terminal

最近遇到个意外的情况:在未知情况下,Chrome 浏览器会对部分 GET 请求缓存,即该请求的 SIZE 指示为 from disk cache,且 cache 返回的状态码为 410

查看 MDN 上对 HTTP 410 的解释为,Gone 服务器返回该状态码,指示该请求资源在服务器上已不可用,而且这个不可用可能是永久的。如果服务器不清楚该资源的丢失是临时的或者是永久的,那么应返回 404,即 Not Found

另外 410 response 是可被缓存的

考虑到实际我们项目中的开发流程,有 Dev / Alpha / Production 环境,各个环境的访问需要切换 proxy 访问,可能存在 CORS (Cross-Origin-Resource-Sharing) 问题,具体如

Alpha 环境域名为 A,因此若访问 Alpha 环境,则将 A domain 配置至主机 hosts 文件中,静态解析 A domain,使其对应 IP 为 Alpha 环境 IP

Production 环境域名也为 A,访问 Production 环境,可以直接公网访问

对于浏览器来说,访问 Alpha / Production 环境的最大不同,为 Remote Address 不同 (域名实际相同)

那么是否有可能为成功的请求访问的 Alpha 环境,而不成功的请求 (被 cache 410 的请求) 访问的为 Production 环境?

顺着这个可疑点开始搜索相关资料,了解到

Prefight request

客户端在发起 COR 请求时,会首先发起 prefight 请求,检查是否对端 Server 接受 COR 请求

client option request

1
2
3
4
OPTIONS /resource/foo 
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org

if server accept this request then it will response a reponse body like

1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

好了,看到这,发现调查的方向有点偏了,切换 proxy 并不会导致 CORS 问题。切换代理之后,会导致后续请求由代理 A 切换至代理 B 转发,仅此而已

Can it be cache by accidently ?

查看代码后,发现后端部分请求在某一时段,的确返回了 410 错误,而 410 错误时,会返回一个 html 页面结果,是否 nginx 对这个结果,设置了有效的 cache-control 导致,浏览器缓存了发生该错误时的请求?

查看 Nginx 的文档后发现,Nginx add_header 仅在特定的 http status code 生效

1
Adds the specified field to a response header provided that the response code equals 200, 201 (1.3.10), 204, 206, 301, 302, 303, 304, 307 (1.1.16, 1.0.13), or 308 (1.13.0). The value can contain variables.

所以如果特定 http 请求,本应返回正常的 json 结构体,然而后台报错,抛出异常,而该异常又未被捕获,因此 http 请求最后获取到的是 tomcat 的 exception 页面,比如 410 的错误页面

又因为未指定默认的 cache 方式,因此该返回没有 cache 相关的 http header,因此全凭浏览器的启发式 cache 策略,意外将该错误的 http 请求返回结果缓存下来

为解决这个问题,可以在 conf/web.xml 中配置

1
2
3
4
5
6
7
8
9
<security-constraint>
<web-resource-collection>
<web-resource-name>HTTPSOnly</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>

即可,这样默认每个请求的返回头都会加上

1
2
Cache-Control: private
Expires: Thu, 01 Jan 1970 00:00:00 GMT

至于为何后端会概率性返回 410,那又是另外一个问题了,后续有机会再说

回顾

因此问题是这样的,对于返回“正常”的请求 Nginx 设置了如下

1
2
3
4
5
6
7
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block;
Strict-Transport-Security: max-age=31536000; includeSubdomains;

请求头

对于静态资源文件,如 .html/.css/.js 等,Nginx 使用了 Expires 指令

1
2
3
4
5
6
7
Cache-Control: max-age=604800
Expires: Sun, 27 May 2018 10:28:04 GMT
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block;
Strict-Transport-Security: max-age=31536000; includeSubdomains;

因此增加或修改了

1
2
Cache-Control: max-age=604800
Expires: Sun, 27 May 2018 10:28:04 GMT

返回请求头

对于非“正常”的请求,使用 Tomcat CONFIDENTIAL 配置,使其返回请求头中默认携带

1
2
Cache-Control: private
Expires: Thu, 01 Jan 1970 00:00:00 GMT

因此浏览器不会缓存错误的返回结果。当然这么配置之后,实际上是所有返回头均有上述字段,一般来说 Tomcat 前端会有 LB,最常见的如 Nginx,Nginx 对资源文件默认设置了 Expires,该指令会修改 Cache-Control / Expires,因此从通用的角度来说,足以解决缓存带来的各种烦人问题,又不至于太影响性能

参考

https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410

https://developer.mozilla.org/en-US/docs/Glossary/cacheable

https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

https://lists.w3.org/Archives/Public/www-archive/2017Aug/0000.html

https://bugs.chromium.org/p/chromium/issues/detail?id=260239

https://stackoverflow.com/questions/21829553/tomcat-security-constraint-impact-cache

v1.7.16

the workflow of the scheduler in kubernetes,开门见山的说,所谓云主机管理也好,网络流量管理也好,总得有个调度员,控制虚拟机实际被运行于哪台物理机中,管理网络流量的走向等等,那么今天看的 scheduler 组件即为 k8s 中的 Pod 调度器组件

概括的说,scheduler 发现有需要调度的 Pod 时,使用注册好的各种策略,进行备选节点筛选及排序,最后选出pod 应被运行于的节点,即完成了其任务

overview of scheduler

从入口看起,so that is
plugin/pkg/scheduler/scheduler.go

1
go wait.Until(sched.scheduleOne, 0, sched.config.StopEverything)

即无限执行 sched.scheduleOne,那么 sched.scheduleOne 又干了啥,主要的逻辑如下

  • 获取待调度的 Pod (sched.config.NextPod())
  • 获取该 Pod 被调度到的节点 (sched.schedule(pod))
  • 并发 bind the pod to its host

schedule of scheduler

plugin/pkg/scheduler/core/generic_scheduler.go

默认 schedule 的实现在 generic_scheduler.go 中,实现了调度接口方法 Schedule
Schedule 的方法又完成了下述两个过程

  • predicates
  • prioritizing

predicates,即强制的过滤策略,使用 predicate 过滤出符合条件的节点

prioritizing,即基于优先级的优选策略,给节点打分,选择得分高的节点

得分排序函数实现

plugin/scheduler/api/types.go

1
2
3
4
5
6
func (h HostPriorityList) Less(i, j int) bool {
if h[i].Score == h[j].Score {
return h[i].Host < h[j].Host
}
return h[i].Score < h[j].Score
}

即 Score 升序排列,在 Score 相等时,host (节点名称) 的字典序在前的优先。得分相同的节点,有 lastNodeIndex round-robin 的方式选择节点

plugin/pkg/scheduler/core/generic_scheduler.go

1
2
3
4
5
firstAfterMaxScore := sort.Search(len(priorityList), func(i int) bool { return priorityList[i].Score < maxScore })
g.lastNodeIndexLock.Lock()
ix := int(g.lastNodeIndex % uint64(firstAfterMaxScore))
g.lastNodeIndex++
g.lastNodeIndexLock.Unlock()

F.A.Q of scheduler

  • scheduler 调度的单位是?

Pod

  • 什么状态的 Pod 会被调度?

待调度的 Pod 会从 podQueue 中被 pop 出,作为 NextPod() 的返回

  • podQueue 什么时候添加 pod?

scheduler 的 config 中使用了 podInformer,仅关注未被 assigned 和非 Succeeded 或者 Failed 的 Pod

plugin/pkg/scheduler/factory/factory.go

1
2
3
4
5
6
7
8
9
10
// unassignedNonTerminatedPod selects pods that are unassigned and non-terminal.
func unassignedNonTerminatedPod(pod *v1.Pod) bool {
if len(pod.Spec.NodeName) != 0 {
return false
}
if pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed {
return false
}
return true
}

scheduler 为 podInformer 提供了 Pod add/update/delete 操作 podQueue 的方法,因此是在此处更新的 podQueue (add 和 update 逻辑实际相同)。另外 podQueue 内部有去重,如果是相同的 pod,则不再入队

plugin/pkg/scheduler/factory/factory.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
podInformer.Informer().AddEventHandler(
cache.FilteringResourceEventHandler{
FilterFunc: func(obj interface{}) bool {
...
},
Handler: cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
c.podQueue.Add(obj);
},
UpdateFunc: func(oldObj, newObj interface{}) {
c.podQueue.Update(newObj);
},
DeleteFunc: func(obj interface{}) {
c.podQueue.Delete(obj);
},
},
},
)

注意 podInformer 还用于维护 scheduler 的 cache

向节点中增加 Pod

plugin/pkg/scheduler/schedulercache/cache.go

1
2
3
4
5
6
7
8
func (cache *schedulerCache) addPod(pod *v1.Pod) {
n, ok := cache.nodes[pod.Spec.NodeName]
if !ok {
n = NewNodeInfo()
cache.nodes[pod.Spec.NodeName] = n
}
n.addPod(pod)
}

计算节点资源

plugin/pkg/scheduler/schedulercache/node_info.go

1
2
3
4
5
6
7
8
9
10
11
// addPod adds pod information to this NodeInfo.
func (n *NodeInfo) addPod(pod *v1.Pod) {
res, non0_cpu, non0_mem := calculateResource(pod)
n.requestedResource.MilliCPU += res.MilliCPU
n.requestedResource.Memory += res.Memory

...
// Consume ports when pods added.
n.updateUsedPorts(pod, true)
n.generation++
}
  • scheduler 的 node 从哪里获取?

套路与 Pod 一致,scheduler 使用了 nodeInformer,add/update/delete 操作 node cache,于是 scheduler 可见所有可使用的 node 资源

plugin/pkg/scheduler/factory/factory.go

1
2
3
4
5
6
7
8
9
// Only nodes in the "Ready" condition with status == "True" are schedulable
nodeInformer.Informer().AddEventHandlerWithResyncPeriod(
cache.ResourceEventHandlerFuncs{
AddFunc: c.addNodeToCache,
UpdateFunc: c.updateNodeInCache,
DeleteFunc: c.deleteNodeFromCache,
},
0,
)
  • scheduler 如何调度 pod 的?
  1. 从 nodeList (nodeInformer 中来) 获取 nodes
  2. Computing predicates
  3. Prioritizing
  4. Selecting host (按得分排序,相同得分的 round-robin)
  • predicates 有哪些?

重要的如

PodFitsResources

计算当前 node 的资源是否能满足 Pod Request,注意 init-container 是串行运行的,因此其所需要的资源,取各个资源维度的最大值,而其他正常的 container 为并行运行的,因此其所需要的资源,取各个资源维度的总和,最后一个 pod 所需要的资源,为 init-container 的最大值与正常 container 的资源总和的较大值

plugin/pkg/scheduler/algorithm/predicates/predicates.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Returns a *schedulercache.Resource that covers the largest width in each
// resource dimension. Because init-containers run sequentially, we collect the
// max in each dimension iteratively. In contrast, we sum the resource vectors
// for regular containers since they run simultaneously.
//
// Example:
//
// Pod:
// InitContainers
// IC1:
// CPU: 2
// Memory: 1G
// IC2:
// CPU: 2
// Memory: 3G
// Containers
// C1:
// CPU: 2
// Memory: 1G
// C2:
// CPU: 1
// Memory: 1G
//
// Result: CPU: 3, Memory: 3G

PodMatchNodeSelector

即 pod 只能被调度至 pod.Spec.NodeSelector 指定的节点上

PodFitsHost

即 pod 只能调度至 pod.Spec.NodeName 的节点上

InterPodAffinityMatches

  1. 检查当前 pod 如被调度到节点上,是否会破坏当前节点上的 pod 的反亲和性

  2. 检查当前 pod 如被调度到节点上,是否满足亲和性及反亲和性

CheckNodeMemoryPressurePredicate

当 pod 的 QoS 为 BestEffort 时 (即没一个 container 设置 resource request/limit 时),需检查当前 node 是否有内存压力

CheckNodeDiskPressurePredicate

检查 node 是否有磁盘压力

等其他 predicates

  • prioritizing 有哪些?

即优选策略,尽可能的将 pod 部署到不同的 zone 不同的 node,平衡 node 的资源使用等

CalculateSpreadPriority

plugin/pkg/scheduler/algorithm/priorities/selector_spreading.go

1
2
3
// CalculateSpreadPriority spreads pods across hosts and zones, considering pods belonging to the same service or replication controller.
// When a pod is scheduled, it looks for services, RCs or RSs that match the pod, then finds existing pods that match those selectors.
// It favors nodes that have fewer existing matching pods.

即 soft-anti-affinity

BalancedResourceAllocationMap

平衡节点资源分配

值得注意的是 prioritizing 返回的均为 HostPriority,当前集群的所有节点会组成 HostPriorityList,可以形象的理解为,经过所有的 prioritizing 后,可以绘制出 node 的条形图,条形即为每个节点的得分,scheduler 最终会推荐得分最高的 node 给 pod

等其他 prioritizing

  • binding ?
  1. 调用 apiserver 接口发送 post binding 请求 sched.config.Binder.Bind(b)

  2. binding 发送之后,调用 sched.config.SchedulerCache.FinishBinding(assumed)

FinishBinding 将 pod 信息附带 ttl 记入 cache,ttl 过期后,从 cache 中删除

为啥在 binding 结束后,还需要如此大费周折的维护 binding 的 ttl cache ?有什么意义呢?

当然有意义,回过开头去看,我们在探寻 podQueue 的 pod 在何处加入时,发现 scheduler 使用了 podInformer,当 podInformer 获得未被调度的 pod 时将这些 pod 加入 podQueue 等待调度

而另外一处 podInformer 则是设置已被调度的 pod 的 add/update/delete 的事件回调,用来同步 cache,若发现 assumePod 已被调度,则从 cache 中删除,又或者 assumePod 已过 ttl 被 cache 删除,则重新 cache.addPod

plugin/pkg/scheduler/factory/factory.go

1
2
3
4
5
6
7
8
9
10
11
12
13
// scheduled pod cache
podInformer.Informer().AddEventHandler(
cache.FilteringResourceEventHandler{
FilterFunc: func(obj interface{}) bool {
...
},
Handler: cache.ResourceEventHandlerFuncs{
AddFunc: c.addPodToCache,
UpdateFunc: c.updatePodInCache,
DeleteFunc: c.deletePodFromCache,
},
},
)

that’s it

可见 scheduler 基于 informer,实时关注 pod 和 node 状态,获取到待调度的 pod 后,根据 predicate 和 prioritizing 策略,为 pod 选出合适的 node,最后并发完成 pod 和 node 的 binding,完成一次调度过程

更详细来说的话 (可能有误,欢迎大家交流斧正)

part of kube-scheduler

当有新的 pod 创建时,the podInformer of scheduler 一方面过滤未被调度且非 Succeed/Failed 状态的 pod, 触发 add 事件,scheduler 将其加入 podQueue 中等待调度,schedulerOne 方法循环执行,每次从 podQueue 中取出一个 pod,根据 predicates / priorities 策略选出 suggested host,assume 该 pod 被调度到 suggested host 上,更新 pod.Spec.NodeName 字段 (仅为后续 addPod,并不影响实际 etcd 中的 pod 对象),随后开始并发 (goroutine) binding,即调用 kube-apiserver api post a binding RPC,随后 finishingBinding,在 cache 中记录该 pod

cache 会定时扫描其中的 assumedPods 信息,若 pod 被 assumed 且超过了 ttl,则删除该 pod (该 pod 所占用的节点资源也被释放)

the podInformer of scheduler 另一方面过滤已被调度(pod.Spec.NodeName 非空)且非 Succeed/Failed 状态的 pod,触发 add/update/delete 事件等,观察到 assumedPod 被调度后,即从 cache 中删除该 pod

part of kube-apiserver

kube-apiserver 在接收到创建 binding 对象请求后,执行 assignPod 方法,最后在 setPodHostAndAnnotations 方法中,将 pod.Spec.NodeName 写入 etcd 中

pkg/pod/registry/core/pod/storage/storage.go

1
2
3
4
5
6
7
8
9
10
11
// Create ensures a pod is bound to a specific host.
func (r *BindingREST) Create(ctx genericapirequest.Context, obj runtime.Object, includeUninitialized bool) (out runtime.Object, err error) {
binding := obj.(*api.Binding)
// TODO: move me to a binding strategy
if errs := validation.ValidatePodBinding(binding); len(errs) != 0 {
return nil, errs.ToAggregate()
}
err = r.assignPod(ctx, binding.Name, binding.Target.Name, binding.Annotations)
out = &metav1.Status{Status: metav1.StatusSuccess}
return
}

part of kubelet

kubelet 启动时,list-watch apiserver

pkg/kubelet/config/apiserver.go

1
2
3
4
5
// NewSourceApiserver creates a config source that watches and pulls from the apiserver.
func NewSourceApiserver(c clientset.Interface, nodeName types.NodeName, updates chan<- interface{}) {
lw := cache.NewListWatchFromClient(c.Core().RESTClient(), "pods", metav1.NamespaceAll, fields.OneTermEqualSelector(api.PodHostField, string(nodeName)))
newSourceApiserverFromLW(lw, updates)
}

kubelet 使用 selector list-watch apiserver,这个 selector 即为 api.PodHostField=nodeName pod.Spec.NodeName=nodeName。kubelet list-watch 被调度本节点上的 pod,当触发 add/update/delete 后做相应的操作

k8s 1.7 闲暇记录

CronJob 一直到 1.8 才开启,1.7 以下的集群需在 apiserver 的启动参数中增加变量,显示开启特定的 api 版本

k8s 中的 job 一般说来需要业务自身做到幂等,或者即使会被重复执行而不影响功能

Job

Job 相比于 StatefulSet / Deployment 的特殊字段

  • .spec.restartPolicy: 仅支持 OnFailed / Never,两种方式控制范围不同,前者当 Pod 容器失败退出时,重启容器,后者当 Pod 容器失败退出时,新建 Pod,会导致 Pod 中的所有容器重启
  • .spec.completions: 完成数,即在 completion 个 pod 执行成功后,认为 Job 完成
  • .spec.parallelism: 并发数,即允许同时执行的 pod 数
  • .spec.activeDeadlineSeconds: Job 执行时间的上限,若超过上限时间仍未完成则 Job 状态变为 DeadlineExceeded,不会再有新的 Pod 被创建,并且已存在的 Pod 将会被删除

通过 completions 和 parallelism 的组合设置,可以达到如下几种 Job 的执行效果

  • 一次性任务

completions =1 && parallelism = 1

  • 固定结束次数任务

completions > 1 && parallelism = 1

  • 并行任务

completions = 1 && parallelism > 1

  • 自定义任务

completions >=1 && parallelism >=1

Job 的接口

1
/apis/batch/v1/namespaces/{namespace}/jobs

Job example yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: batch/v1
kind: Job
metadata:
name: busybox
spec:
activeDeadlineSeconds: 100
parallelism: 1
completions: 1
template:
metadata:
name: busybox
spec:
containers:
- name: busybox
image: busybox
command: ["echo", "hello"]
restartPolicy: Never

CronJob

顾名思义,定时任务,支持类似 linux cron 的定时策略,定时调度 Job,可以这么理解 CronJob 控制 Job,而 Job 控制 Pod,Pod 完成具体的业务逻辑

CronJob 的特殊字段

  • .spec.schedule: core of cronjob and it is like one line of a crontab (cron table) file,即定时策略配置,例如 */1 * * * *,每分钟调度一次 Job 执行
  • .spec.startingDeadlineSeconds: 调度 Job 最大开始时间,如果错过任务执行,错过的工作执行将被视为是失败的任务
  • .spec.concurrencyPolicy: Allow/Forbid/Replace,即允许并行执行任务,Forbid 不允许并行执行任务,Replace 取消当前执行的任务,并新建一个任务取代它;考虑任务执行时间较长,而定时间隔较短的情况下,该字段的意义明显
  • .spec.suspend: 暂停调度任务,不影响已调度的任务
  • .spec.successfulJobsHistoryLimit: 保留成功执行的任务记录数
  • .spec.failedJobsHistoryLimit: 保留执行失败的任务记录数

注意 CronJob 在 1.7 中仍然为 Alpha 版本,接口为

1
/apis/batch/v2alpha1/namespaces/{namespace}/cronjobs

CronJob example yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: batch/v2alpha1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
args:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure

由 CronJob 创建的 Job,在 Job 的 metadata 字段的 ObjectReference 有所体现,会写明是由 cronJob controller 控制

Overview

查看了 job/cronjob 的功能后,我们发现 job 适合用来执行一些初始化 / 统计数据 / 备份 / 清理工作,即那些不需要一直运行的工作,需要长期运行的工作,当然还是 Deployment/StatefulSet 更合适了

how to create a bucket in bbolt

fresh new db file

page 3 (start from 0) is a leaf page, it will be used as a root bucket

1
2
3
4
type bucket struct {
root pgid // page id of the bucket's root-level page
sequence uint64 // monotonically incrementing, used by NextSequence()
}
1
m.root = bucket{root: 3}

bucket 结构表示存储于文件中的 bucket

另外 tx 会关联一个 Bucket 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Bucket struct {
*bucket
tx *Tx // the associated transaction
buckets map[string]*Bucket // subbucket cache
page *page // inline page reference
rootNode *node // materialized node for the root page.
nodes map[pgid]*node // node cache
// Sets the threshold for filling nodes when they split. By default,
// the bucket will fill to 50% but it can be useful to increase this
// amount if you know that your write workloads are mostly append-only.
//
// This is non-persisted across transactions so it must be set in every Tx.
FillPercent float64
}

可见其中组合了 bucket 结构体

写事务在初始化时,会使用 meta 锁,锁定住 meta 页的修改;随后将 meta 页拷贝至写事务内部存储;而实际上写事务开启时,会使用 rwlock,因此写事务并不会并发,另仅有写事务会修改 meta 页,所以此处的 meta 页拷贝存疑,似乎没必要

init 方法为 beginTX 内部执行,读写事务都会执行,因此虽然写事务无需 copy meta page 然而读事务需要,因为写事务 commit 之后,会修改 meta page

完成 meta 页的拷贝后,将 tx 的 root (Bucket) 初始化,并设置其 root bucket 为 meta 中的 root bucket; 第一个写事务的 txid 为 2,0、1 用于设置两个 meta 页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for i := 0; i < 2; i++ {
p := db.pageInBuffer(buf[:], pgid(i))
p.id = pgid(i)
p.flags = metaPageFlag
// Initialize the meta page.
m := p.meta()
m.magic = magic
m.version = version
m.pageSize = uint32(db.pageSize)
m.freelist = 2
m.root = bucket{root: 3}
m.pgid = 4
m.txid = txid(i) // 0 1 txid used
m.checksum = m.sum64()
}

create bucket 时 cursor 从 root bucket page 开始遍历 bucket name 应存放的适当位置

branch page 节点 / leaf page 节点

数据存放于 leaf page 节点中

存储于文件中的为 page,内存中的为 node,从文件中读取到的 page 会 materialed 为内存中的 node

机缘巧合,在测试的引导下,读了下 etcd 连接建立方面的代码

etcd 启动后监听 peer url

peer url 通过 mux 绑定 handler,关于 raft 的 url 的请求绑定到 streamHandler 上,这玩意会 hold 住一个连接,除非遇到错误,<-c.closeNotify(),连接 close

啥时候重新 p.attachOutgoingConn(conn) 回来,当然是该成员又请求连接到 url 上来时,即 streamReader 重新连接回来时

streamWriter 使用长链

streamReader 持续读,与 streamWriter 匹配,streamWriter 不遇到错误,不 close 连接;streamReader 断了之后,100ms 重新 dial 一次,重连上后,对端 streamWriter 能 hold 住新的连接

etcd 对其每一个 peer 都会启动 streamReader 和 streamWriter,reader 建立连接后,writer 使用不关闭,reader 有数据时读,writer 有写入时写,保持着连接

所以 etcd peer 间是建立着长链的,可以使用 netstat -anp | grep {etcd_peer_port} 查看 peer 之间的连接建立情况

安全方面的姿势,掌握略有不足,趁着空闲;另外也是为了不仅仅知道,curl 命令访问 https 接口的时候,需要携带三个证书,如此模糊的解释而努力
to be cont.

搜索了一下网上已有很多现有资料,这里就重新回顾一下,当做我自己的姿势了

https://github.com/denji/golang-tls

首先看下 go 语言中,如何实现 server 端 HTTPS/TLS

http://tonybai.com/2015/04/30/go-and-https/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
// "fmt"
// "io"
"net/http"
"log"
)
func HelloServer(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("This is an example server.\n"))
// fmt.Fprintf(w, "This is an example server.\n")
// io.WriteString(w, "This is an example server.\n")
}
func main() {
http.HandleFunc("/hello", HelloServer)
err := http.ListenAndServeTLS(":443", "server.crt", "server.key", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

443 为知名的 HTTPS 服务端口,那么 server.crt、server.key 这两个文件又是如何作用,哪来的呢?

首先解释哪来的问题

使用 openssl 生成私钥

1
2
# Key considerations for algorithm "RSA" ≥ 2048-bit
openssl genrsa -out server.key 2048

or 使用另外一种算法生成的私钥

1
2
3
# Key considerations for algorithm "ECDSA" ≥ secp384r1
# List ECDSA the supported curves (openssl ecparam -list_curves)
openssl ecparam -genkey -name secp384r1 -out server.key

私钥生成好之后,使用私钥生成公钥(x509 自签发 crt)

Generation of self-signed(x509) public key (PEM-encodings .pem|.crt) based on the private (.key)

1
openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650

所以呢,server.key 是私钥,server.crt 是公钥,生成之后,就可以用来初始化 TLS server 了

前言

最近在给 ETCD cluster on k8s 写 FE (front end),此篇总结一下框架性的东西

很久之前在实验室的时候,曾经蹚水过一段时间 fe 开发,深知 fe 领域目前 一天涌现 100 个开发工具 的节奏,从 angularjs (google) 到 react (facebook),都是 SPA (single page application) 的实践

使用这两框架,对于 fe 小白开发来说,最大好处是省去了大部分 jQuery 手工操作 DOM 的繁杂代码,都由框架代为更新 DOM 元素了。当然也引入了比服务器端渲染页面的经典设计模式 MVC (model view controller),更进一步的 MVVM (model view viewModel) 模式,支持视图到模型,模型到视图的双向数据更新特性。由此 fe 的代码得到极大净化

然而无奈 fe 仍然是个劳动密集型的方向,毕竟是眼见为实,与用户距离最近的东西,一言不合就有需求,就有改动了。因此代码一开始可能是规整的,过了一段时间后,就直接起飞了 …

现实不讨论了,先进入正题

找轮子

不重复造轮子,github 上搜索一把,可以得到很多 startup 项目,找一个 star 比较多的,例如 https://github.com/preboot/angularjs-webpack,直接用该项目来开始好了

1
git clone https://github.com/preboot/angularjs-webpack.git

分析轮子

该项目为 node + angularjs + webpack 的一个极简 demo

node 就不说了,fe 的革命,很大程度由 node 引发

angularjs 呢,mvvm 框架

webpack 简单理解的话,在 java / c++ 等语言中,可以通过 include or import 关键字导入依赖的库,进而在当前模块中使用已实现的方法,避免重复的开发工作。那么在 fe 中 import 依赖的组件,如当前模块依赖的 js / css 代码,webpack 的作用就是理解这些 import 指令,最后将所有代码 打包 成可实际执行的代码

用轮子造车子

项目结构

1
2
3
4
5
6
7
8
├── LICENSE
├── README.md
├── karma.conf.js
├── node_modules
├── package.json
├── postcss.config.js
├── src
└── webpack.config.js

package.json 定义了 node 项目的依赖

通过 npm install 安装 package.json 中定义的依赖到项目下的 node_modules 文件夹下

国内的网络环境一般,需要一些手段加速依赖下载,如淘宝的 npm 镜像站

1
2
3
4
# 安装淘宝定义的 cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
# 安装项目依赖
cnpm install

速度可以说是很快了,秒装

webpack.config.js 为 webpack 的配置文件,其中比较重要的配置有

SPA 应用 js 入口

1
2
3
config.entry = isTest ? void 0 : {
app: './src/app/app.js'
};

SPA 应用 page 入口

1
2
3
4
new HtmlWebpackPlugin({
template: './src/public/index.html',
inject: 'body'
}),

base 路径

1
2
3
4
config.devServer = {
contentBase: './src/public',
stats: 'minimal'
};

即在该路径下有一文件,如 ./src/public/hello.png,那么在浏览器中 url/hello.png 能访问到

本地开发时 dev server 的访问地址

1
2
3
// Output path from the view of the page
// Uses webpack-dev-server in development
publicPath: isProd ? '/' : 'http://localhost:8080/',

本地开发

1
2
3
4
// 启动 webpack dev server
npm start
// 浏览器访问 pulicPath 地址即可,如
// http://localhost:8080/

其他的不多说了,此篇质量一般,也是我现在开发 fe 的一个无奈吧,这里增加几句话,那里增加几句话,okay it works,细节不清楚,只是为了完成业务逻辑,当然也因为目前兴趣不在此。详细的可看看 参考 (3)

参考

https://github.com/preboot/angularjs-webpack

https://npm.taobao.org/

http://angular-tips.com/blog/2015/06/using-angular-1-dot-x-with-es6-and-webpack/

0%