背景 去年年底在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 SocketUnix 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-servercode-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来实现方便快捷, 同时也不需要太过于担心入口的性能问题