从单体到微服务,腾讯文档微服务网关工程化的演进实践
👉导读
👉目录
01
1.1 TL;DR
1.2 问题表现
1.3 问题根源
1.4 为什么锁不住?
@svr/proxy@0.0.123 /app`-- @wgw/tools@0.0.123+-- @opentelemetry/exporter-trace-otlp-grpc@0.27.0| `-- @grpc/grpc-js@1.9.5+-- @tencent/polaris@0.4.3| `-- @grpc/grpc-js@1.9.5 deduped+-- @tencent/tdocs-common-rainbow@0.0.4| +-- @tencent/rainbow-node-admin-sdk@0.2.56| | +-- @tencent/polaris@0.4.5| | | `-- @grpc/grpc-js@1.9.5 deduped| | `-- @tencent/trpc-rpc-client@0.5.39| | `-- @tencent/trpc-rpc-naming@0.5.39| | `-- @tencent/polaris@0.3.36| | `-- @grpc/grpc-js@1.3.8| `-- @tencent/rainbow-node-sdk@0.2.66| `-- @tencent/polaris@0.5.2| `-- @grpc/grpc-js@1.9.5 deduped`-- @tencent/trpc-rpc-client@0.7.3`-- @tencent/trpc-rpc-naming@0.7.3`-- @tencent/polaris@0.5.2 // ^0.5.2`-- @grpc/grpc-js@1.9.5 deduped根目录的 @grpc/grpc-js 的依赖就是 1.9.5
1.5 为什么没有使用 lock 文件?
> tree -L 2 -I "node_modules".├── lerna.json├── package.json├── packages -- 这里是四个微服务共用的包逻辑 @wgw/xxx│ ├── deploy│ ├── helpers│ ├── middleware│ ├── proto│ ├── rpc│ ├── tools│ ├── types│ └── utils├── README.md├── servers -- 四个微服务的服务入口 @svr/xxx│ ├── cgi│ ├── edit│ ├── proxy│ └── static├── tsconfig.base.json├── tsconfig.build.json├── tsconfig.json└── yarn.lock
1.6 确定优化方向
Docker 镜像打包需要打包完整的 node_modules 且优化后项目启动入口文件不要有过多变化。 工程化工具需要解决间接依赖锁定版本问题,并能自定义依赖的具体版本。
02
2.1 高内聚低耦合
2.2 代码组织的极端
2.3 pnpm workspace
03
3.1 Docker 镜像构建
3.2 Docker COPY 与 node_modules
COPY . .
FROM base AS topdepWORKDIR /RUN mkdir node_modules packagesCOPY node_modules/ node_modulesCOPY packages/ packagesFROM topdep as svrWORKDIR /usr/appCOPY ["pnpm-*.yaml", ".npmrc", "./"]COPY "servers/${APP}/" .
3.3 Docker context
tar --exclude='node_modules' --exclude='.git' -cf - ../../ | docker build -f servers/proxy/Dockerfile.context - -t contextproxysvr3.4 pnpm-context.mjs
// .... 这里只展示入口逻辑async function main(cli) {const projectPath = dirname(cli.dockerFile)// https://pnpm.io/filteringconst [dependencyFiles, packageFiles, metaFiles] = await Promise.all([getFilesFromPnpmSelector(`{${projectPath}}^...`, cli.root, {extraPatterns: cli.extraPatterns}),getFilesFromPnpmSelector(`{${projectPath}}`, cli.root, {// 本包目录下除了 Dockerfile 都复制extraPatterns: cli.extraPatterns.concat([`!${cli.dockerFile}`])}),getMetafilesFromPnpmSelector(`{${projectPath}}...`, cli.root, {extraPatterns: cli.extraPatterns})])await withTmpDir(async (tmpdir) => {await Promise.all([fs.copyFile(cli.dockerFile, join(tmpdir, 'Dockerfile')),copyFiles(dependencyFiles, join(tmpdir, 'deps')),copyFiles(metaFiles, join(tmpdir, 'meta')),copyFiles(packageFiles, join(tmpdir, 'pkg'))])const files = await getFiles(tmpdir)if (cli.listFiles) {for (const path of files) console.log(path)} else {await pipe(createTar({ gzip: true, cwd: tmpdir }, files), process.stdout)}})}// ...
# 复制构建后的代码COPY ./meta .COPY ./deps .COPY ./pkg .RUN --mount=type=cache,id=pnpm-store,target=/root/pnpm-store\pnpm config set store-dir /root/pnpm-store && pnpm install --filter "{${PACKAGE_PATH}}..."\--frozen-lockfile\--prod\--ignore-scripts\--unsafe-perm
3.5 node-linker & package-import-method
package-import-method=copy
3.6 软链接与硬链接
04
4.1 幽灵依赖问题解决
由于依赖被打平,B 依赖被提升到 node_modules 的一级目录node_modules├── A@1.0.0├── B@1.0.0└── C@1.0.0└── node_modules└── B@2.0.0项目只引用了 A 和 C,但是如果代码里面写了引用 B 的代码,也是可以引用到的。{"dependencies": {"A": "^1.0.0","C": "^1.0.0"}}
4.2 .pnpmfile.mjs 彻底解决依赖(直接或间接)锁版本问题
// 类型信息是我根据 pnpm 源码找到添加上去的, 可以帮助编写逻辑/*** @typedef PackageInfo* @type {object}* @property {string} name* @property {string} version* @property {string} description* @property {string} main* @property {object} scripts* @property {string} author* @property {string} license* @property {object} dependencies* @property {object} devDependencies* @property {object} optionalDependencies* @property {object} peerDependencies*//*** @typedef LockfileProjectSnapshot Map 类型都不是真正的 Map,只是对象* @type {object}* @property {Map<string, string>} specifiers* @property {Map<string, string>} dependencies* @property {Map<string, string>} devDependencies* @property {Map<string, string>} optionalDependencies* @property {object} dependenciesMeta* @property {string} publishDirectory*//*** @typedef Lockfile Map 类型都不是真正的 Map,只是对象* @type {object}* @property {Map<string, LockfileProjectSnapshot>} importers 引包入口* @property {string | number} lockfileVersion* @property {LockfileProjectSnapshot} packages*//*** @typedef ReadPackageContext* @type {object}* @property {function} log*//*** Will be called after parse package dependencies* @param {PackageInfo} pkg* @param {ReadPackageContext} context* @returns*/function readPackage(pkg, context) {// 解决直接依赖版本if (pkg && pkg.dependencies && pkg.dependencies['@grpc/grpc-js']) {const grpcVersion = pkg.dependencies['@grpc/grpc-js']if (compareVersion(grpcVersion, '1.7.3') >= 1) {pkg.dependencies['@grpc/grpc-js'] = '1.7.3'context.log(`Modifying the @grpc/grpc-js package(which greater than 1.7.3) to be version 1.7.3 in ${pkg.name}`)}}return pkg}/*** Will be called before preinstall to modify lockfile to lock package version* @param {Lockfile} lockfile* @param {ReadPackageContext} context* @returns*/function afterAllResolved(lockfile, context) {// 直接依赖从上面 readPackage 里面来解决// 这里是解决间接依赖的问题/*** 以下逻辑:* 1. 检查所有包生产依赖有没有依赖到 @grpc/grpc-js,如有则判断版本是否超过 1.7.3,* 如超过则强行修改依赖为 1.7.3(因为不希望引到超过这个版本,超过这个版本会有 TCP 占用问题)* 2. 检查包本身的信息是否是 @grpc/grpc-js 这个包,如果超过则删除* 3. 添加 @grpc/grpc-js/1.7.3 版本的 entry 信息*/if (lockfile.packages) {const allPackageNames = Object.keys(lockfile.packages)for (const packageName of allPackageNames) {const packageInfo = lockfile.packages[packageName]const packageDependencies = packageInfo.dependenciesif (packageName.includes('@grpc/grpc-js')) {const grpcVersionMatchRes = packageName.match(/\\d+\\.\\d+\\.\\d+/)const grpcVersion = grpcVersionMatchRes[0]if (compareVersion(grpcVersion, '1.7.3') >= 1) {delete lockfile.packages[packageName]}}if (packageDependencies && packageDependencies['@grpc/grpc-js'] !== undefined) {packageGRPCDependencyVersion = packageDependencies['@grpc/grpc-js']// 如果版本比 1.7.3 低则不修改, 如果高于 1.7.3 版本则修改为 1.7.3if (compareVersion(packageGRPCDependencyVersion, '1.7.3') >= 1) {packageDependencies['@grpc/grpc-js'] = '1.7.3'}}}// 给 packages 添加 @grpc/[email protected] 版本 entrylockfile.packages['/@grpc/grpc-js/1.7.3'] = grpcLockInfoEntry('1.7.3')}context.log('Modifying the @grpc/grpc-js package(which greater than 1.7.3) to be version 1.7.3')return lockfile}...module.exports = {hooks: {readPackage,afterAllResolved}}
05