背景 去年年底在qcon2020
大会上了解到了某团的微服务网格治理方式, 使用Sidecar + Unix Socket
实现了服务的解耦, 目前有个同样的类似场景, 只需要鉴权功能, 所以考虑参考这个方式来实现
使用场景同样是在qcon
上得到的启发, 外面各大厂都在研究独立在线IDE
, 恰好现在有个需求可以尝试使用这种方式实现, 了解了一番后发现实际上都是基于code-server 来实现的, 而code-server
只提供了最简单的token
鉴权功能, 而我们需要实现对接内部账号, 所以正好使用Sidecar
来实现鉴权
调研 外部的Sidecar
模式是使用Unix Socket
做进程间通讯的, 对比起TCP Socket
通讯来说, 减少了协议解析, 所以理论上能获得更高的性能
相关资料
UNIX Socket
简介这部分是copy
其他资料的, 简单介绍一下UNIX Socket
的概念
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const http = require ('http' );var server = http.createServer((req, res ) => { res.writeHead(200 , { 'Content-Type' : 'text/plain' }); res.end('hello world' ); }); server.listen(3000 , '127.0.0.1' , () => { console .log('在端口3000启动了服务器' ); }); server.listen('/path/to/socket' , () => { console .log('从socket路径启动了服务器' ); });
在上面示例中,使用port
端口形式的监听,本质是监听TCP Socket
端口。而使用path
文件路径的方式,本质上是Unix Domain Socket
(Unix
域套接字)文件。
什么是Socket
在网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个Socket
。Socket
可以被定义描述为两个应用通信通道的端点,一个Socket
端点可以用Socket
地址(地址 IP、端口、协议组成)来描述。Socket
作为一种进程通信机制,操作系统会分配唯一一个Socket
标识,这个标识与通讯协议有关(不仅限于TCP
或UDP
)。
Unix Domain Socket
Unix Domain Socket
并不是一个实际的协议,它只在同客户机和服务器通信时使用的API
,且一台主机与在不同主机间通信时使用相同的API
。
Unix Domain Socket
有以下特点
Unix Domain Socket
使用的地址通常是一个文件
在同一主机通讯时,传输速率是不同主机间的两倍
Unix Domain Socket
套接字描述符可以在同一主机不同进程间传递
Unix Domain Socket
套接字可以向服务器提供用户认证信息
TCP Socket
与Unix Domain Socket
无论是TCP Socket
套接字还是Unix Domain Socket
套接字,每个套接字都是唯一的。TCP Socket
通过IP
和端口描述,而Unix Domain Socket
通过文件路径描述。
TCP
属于传输层的协议,使用TCP Socket
进行通讯时,需要经过传输层TCP/IP
协议的解析。
而Unix Domain Socket
可用于不同进程间的通讯和传递,使用Unix Domain Socket
进行通讯时不需要经过传输层,也不需要使用TCP/IP
协议。所以,理论上讲Unix Domain Socket
具有更好的传输效率。
实现 架构设计 sequenceDiagram
participant B as Browser
participant A as Node Auth Server
participant S as Code Server
participant T as Third Party User Service
alt 用户访问
B ->> A: 1. 访问服务
A ->> T: 2. 获取用户信息
opt 权限验证失败
T -->> A: 3.1 无权限
A -->> B: 4.1 跳转登录页
end
opt 权限验证成功
T -->> A: 3.2 有权限
A ->> S: 4.2 获取数据
S -->> A: 5.2 返回结果
A -->> B: 6.2 返回结果
end
end
可以看到用户实际访问的入口是Sidecar
组件, 而Code Server
服务没有任何改动, 维护起来非常方便
code-server
code-server
提供docker
化部署方式, 同时也支持socket
启动, 只需要手动修改一下启动命令, 将服务监听修改为通过socket
, 并禁用auth
验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 FROM codercom/code-serverRUN sudo apt-get update && sudo apt-get install -y vim RUN sudo apt-get update && sudo apt-get install -y npm RUN sudo npm install -g n RUN sudo n lts RUN sudo npm install -g npm COPY --chown=coder:coder .vscode/user-settings.json /home/coder/.local /share/code-server/User/settings.json COPY --chown=coder:coder .vscode/user-keybindings.json /home/coder/.local /share/code-server/User/keybindings.json RUN mkdir -p /home/coder/project/ WORKDIR /home/coder/project/ ENTRYPOINT ["/bin/bash" , "-c" , "/usr/bin/entrypoint.sh --socket /var/run/unix-socket/server.socket --auth none --disable-telemetry --disable-update-check ./" ]
auth-sidecar
这个服务比较轻量, 却是实际服务的入口, 将所有需要登录的请求验证一次用户身份, 如果不通过就返回302
并跳转登录, 如果通过则通过unix-socket
转发给code-server
获取真实数据
这里稍微注意下需要对websocket
协议同样进行转发处理, 因为code-server
里用到了websocket
连接, 如果不处理, 就会导致无法使用
而websocket
协议同样是携带header
的, 所以可以直接使用通用的鉴权方式
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 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 const http = require ('http' );const httpProxy = require ('http-proxy' );const express = require ('express' );const _ = require ('lodash' );const fetch = require ('node-fetch' ).default;const ServerPort = JSON .parse(process.env.PORT || '8080' );const ProxySocketPath = JSON .parse(process.env.PROXY_SOCKET_PATH || `"/var/run/unix-socket/server.socket"` );const AuthProxyAddr = `...` ;const getUserInfo = async (headers) => { if (!headers.cookie && !headers['x-token' ]) { return null ; } const userPermissionResponse = await fetch(AuthProxyAddr, { timeout : 3000 , headers : { ..._.pick(headers, ['cookie' , 'origin' , 'referer' , 'user-agent' , 'x-token' ]), 'content-type' : 'application/json' , }, }); if (userPermissionResponse.status != 200 ) { console .log(`Auth Error: get user permission info from auth error [${userPermissionResponse.status} ]` ); return null ; } const data = await userPermissionResponse.json(); if (data.code !== 200 ) { console .log(`Auth Error: get user permission info from auth error: ${JSON .stringify(data)} ` ); return null ; } return data.data; }; const validateUser = async (headers) => { const data = await getUserInfo(headers); if (!data) { return false ; } return true ; }; const app = express();const proxy = httpProxy.createProxyServer({ target : { socketPath : ProxySocketPath }, ws : true }).on('error' , (error, req, res ) => { if (!res.destroyed) { res.statusCode = 500 ; res.end(`Server error` ); } console .log(`Proxy error` , error); }); const authProxy = httpProxy .createProxyServer({ target : AuthProxyAddr, changeOrigin : true , xfwd : true , }) .on('error' , (error, req, res ) => { if (!res.destroyed) { res.statusCode = 500 ; res.end(`Server error` ); } console .log(`Proxy error` , error); }); const server = http.createServer(app);server.on('upgrade' , async (req, socket, head) => { if (!(await validateUser(req.headers))) { socket.write(`HTTP/1.1 401 Unauthorized\r\n\r\n` ); socket.destroy(); return ; } proxy.ws(req, socket, head); }); app.route(['/api/auth' , '/login' , '/logout' ]).all((req, res ) => { authProxy.web(req, res, {}); }); app.route('/' ).all(async (req, res) => { if (!(await validateUser(req.headers))) { res.redirect(`/login` ); return ; } proxy.web(req, res, {}); }); app.route('/vscode-remote-resource' ).all(async (req, res) => { if (!(await validateUser(req.headers))) { res.statusCode = 401 ; res.end(); return ; } proxy.web(req, res, {}); }); app.route('*' ).all((req, res ) => { proxy.web(req, res, {}); }); server.on('error' , (error ) => { console .log(`Server error` , error); }); server.listen(ServerPort, () => { console .log(`Server listen ${ServerPort} ` ); });
k8s
启动其中code-server-node
的project
目录是通过持久化存储挂载进去的, 保证了重启不会丢失数据
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 application: containers: - name: code-server-node image: code-server-node command: - /bin/bash - '-c' - /usr/bin/entrypoint.sh --socket /var/run/unix-socket/server.socket --auth none --disable-telemetry --disable-update-check ./ cpu: '2' reqCPU: '2' memory: 4Gi reqMemory: 2Gi volumeMounts: - name: unix-socket-dir mountPath: /var/run/unix-socket/ imagePullPolicy: Always - name: code-server-auth image: code-server-auth cpu: '1' reqCPU: '1' memory: 1Gi reqMemory: 1Gi volumeMounts: - name: unix-socket-dir mountPath: /var/run/unix-socket/ imagePullPolicy: Always volumes: - name: unix-socket-dir emptyDir: {}
小结 开发完成后实测非常好用, 而且是完全无侵入式实现, 对后续的code-server
维护/升级等都无压力, 体验非常好
同时经过这次Sidecar
尝试后, 发现后续很多类似服务设计方案都可以考虑使用这种模式实现
问题 这里仍然存在的问题是, node-auth
往往使用go/c++
等实现, 因为作为主要的流量入口, 往往还是会担心node
性能不够. 但是此次的需求因为并不是外部访问, 所以使用node
来实现方便快捷, 同时也不需要太过于担心入口的性能问题