diff --git a/.gitattributes b/.gitattributes index 2c8b087..6313b56 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -deploy/dev/script/*.sh text eol=lf +* text=auto eol=lf diff --git a/.gitignore b/.gitignore index cc49eaa..a42f474 100644 --- a/.gitignore +++ b/.gitignore @@ -1,128 +1,128 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -dev test -.env.test -.env.production - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* - -# End of https://mrkandreev.name/snippets/gitignore-generator/#Node - -DockerFile -.idea - -# Go compiled binaries -app/*/api/api -app/*/rpc/rpc +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +dev test +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# End of https://mrkandreev.name/snippets/gitignore-generator/#Node + +DockerFile +.idea + +# Go compiled binaries +app/*/api/api +app/*/rpc/rpc app/*/mq/mq \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore index 1c2fda5..13566b8 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,8 +1,8 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml index 7fbf49e..cd630dd 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/.idea/st-1-example.iml b/.idea/st-1-example.iml index 338a266..5e764c4 100644 --- a/.idea/st-1-example.iml +++ b/.idea/st-1-example.iml @@ -1,9 +1,9 @@ - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index c8397c9..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - - - - - + + + + + \ No newline at end of file diff --git a/Herebyfile.mjs b/Herebyfile.mjs index fa9c977..9703529 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -1,200 +1,200 @@ -import { search } from "@inquirer/prompts"; -import { execa } from "execa"; -import Fuse from "fuse.js"; -import { glob } from "glob"; -import { task } from "hereby"; -import path from "node:path"; -import fs from "node:fs/promises"; -import { fileURLToPath } from "node:url"; -import { parseArgs } from "node:util"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const { values } = parseArgs({ - args: process.argv.slice(3), - options: { - server: { type: "string", short: "s", multiple: true }, // 服务名称 - type: { type: "string", short: "t" }, // 生成类型:api, rpc, docker - imageName: { type: "string", short: "i" }, // 镜像名字 - } -}) - -const Paths = { - root: __dirname, - desc: path.join(__dirname, "desc"), - app: path.join(__dirname, "app"), - getServiceName: (filePath) => path.basename(filePath, path.extname(filePath)), - getOutputDir: (serviceName) => path.join(__dirname, "app", serviceName), - pathlistToChoices: (filePaths) => filePaths.map(filePath => ({ - title: Paths.getServiceName(filePath), - value: filePath, - })), - getDescFiles: async (pattern) => { - return await glob(pattern, { cwd: Paths.desc, absolute: true }); - }, - getAllApi: async () => { - const apiPattern = "api/*.api"; - return Paths.pathlistToChoices(await Paths.getDescFiles(apiPattern)); - }, - getAllProto: async () => { - const protoPattern = "rpc/*.proto"; - return Paths.pathlistToChoices(await Paths.getDescFiles(protoPattern)); - }, - getAllservice: async () => { - let all = []; - const services = await fs.readdir(Paths.app); - for (const service of services) { - const servicePath = path.join(Paths.app, service); - const svcTypes = await fs.readdir(servicePath); - svcTypes.map(svcType => all.push({ - title: `${service} - ${svcType}`, - value: path.join(servicePath, svcType, svcType !== "rpc" ? `${service}.go` : "pb.go"), - })); - } - return all; - }, -} - - -const Generators = { - async api(apiFile) { - const serviceName = Paths.getServiceName(apiFile); - const outputDir = path.join(Paths.getOutputDir(serviceName), 'api'); - - await fs.mkdir(outputDir, { recursive: true }); - await run('goctl', [ - 'api', 'go', - '--api', apiFile, - '--dir', outputDir, - '--style', 'goZero' - ]); - }, - async rpc(protoFile) { - const serviceName = Paths.getServiceName(protoFile); - const outputDir = path.join(Paths.getOutputDir(serviceName), 'rpc'); - - await fs.mkdir(outputDir, { recursive: true }); - await run('goctl', [ - 'rpc', 'protoc', protoFile, - `--proto_path=${path.join(Paths.desc, "rpc",)}`, - `--go_out=${outputDir}`, - `--go-grpc_out=${outputDir}`, - `--zrpc_out=${outputDir}`, - '--style=goZero', - ]); - }, - async docker(servicePath) { - const dockerFiles = await glob("DockerFile", { cwd: __dirname, absolute: true }); - if (dockerFiles.length !== 0) { - fs.rm(dockerFiles[0], { force: true }); - } - await run('goctl', [ - "docker", "--go", path.relative(__dirname, servicePath) - ]) - } -}; - - -const GenerateConfig = { - api: { - getChoices: () => Paths.getAllApi(), - prompt: "Select an API description file", - generate: (path) => Generators.api(path), - }, - rpc: { - getChoices: () => Paths.getAllProto(), - prompt: "Select a proto file", - generate: (path) => Generators.rpc(path), - }, - docker: { - getChoices: () => Paths.getAllservice(), - prompt: "Select a service to generate Dockerfile", - generate: (path) => Generators.docker(path), - } -}; - - - -async function run(cmd, args, opts = {}) { - console.log(`>> ${cmd} ${args.join(' ')}`); - return execa(cmd, args, { - stdio: 'inherit', - ...opts - }); -} - - -async function searchSelector(chooses, message) { - const fuse = new Fuse(chooses, { - keys: ['title'], - threshold: 0.4, - }) - return search({ - message, - source: async (term) => { - if (!term) { - return chooses.map(s => ({ name: s.title, value: s.value })); - } - const result = fuse.search(term); - return result.map(s => ({ name: s.item.title, value: s.item.value })); - } - }) -} - - -async function generateHandle() { - const type = values.type; - if (!type || !GenerateConfig[type]) { - console.error("Please specify valid -t "); - return; - } - const config = GenerateConfig[type]; - - const input = values.server - ? (Array.isArray(values.server) ? values.server[0] : values.server) - : await searchSelector(await config.getChoices(), config.prompt); - - await config.generate(input); -} - -async function buildImage(imageName) { - await run("docker", ["build", "-t", imageName, "."]) -} - - -export const init = task({ - name: "init", - desc: "initialize the project", - run: async () => { - await run("go", ["install", "github.com/zeromicro/go-zero/tools/goctl@latest"]); - } -}) - - -export const tidy = task({ - name: "tidy", - desc: "tidy go.mod and go.sum", - run: async () => { run("go", ["mod", "tidy"]) }, -}) - - -export const build = task({ - name: "build", - desc: "build docker image", - run: async () => { - if (values.imageName) { - buildImage(values.imageName); - } else { - console.error("Please specify image name with -i "); - } - } -}) - - -export const gen = task({ - name: "gen", - desc: "generate API/RPC service code", - run: generateHandle, -}); - - +import { search } from "@inquirer/prompts"; +import { execa } from "execa"; +import Fuse from "fuse.js"; +import { glob } from "glob"; +import { task } from "hereby"; +import path from "node:path"; +import fs from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const { values } = parseArgs({ + args: process.argv.slice(3), + options: { + server: { type: "string", short: "s", multiple: true }, // 服务名称 + type: { type: "string", short: "t" }, // 生成类型:api, rpc, docker + imageName: { type: "string", short: "i" }, // 镜像名字 + } +}) + +const Paths = { + root: __dirname, + desc: path.join(__dirname, "desc"), + app: path.join(__dirname, "app"), + getServiceName: (filePath) => path.basename(filePath, path.extname(filePath)), + getOutputDir: (serviceName) => path.join(__dirname, "app", serviceName), + pathlistToChoices: (filePaths) => filePaths.map(filePath => ({ + title: Paths.getServiceName(filePath), + value: filePath, + })), + getDescFiles: async (pattern) => { + return await glob(pattern, { cwd: Paths.desc, absolute: true }); + }, + getAllApi: async () => { + const apiPattern = "api/*.api"; + return Paths.pathlistToChoices(await Paths.getDescFiles(apiPattern)); + }, + getAllProto: async () => { + const protoPattern = "rpc/*.proto"; + return Paths.pathlistToChoices(await Paths.getDescFiles(protoPattern)); + }, + getAllservice: async () => { + let all = []; + const services = await fs.readdir(Paths.app); + for (const service of services) { + const servicePath = path.join(Paths.app, service); + const svcTypes = await fs.readdir(servicePath); + svcTypes.map(svcType => all.push({ + title: `${service} - ${svcType}`, + value: path.join(servicePath, svcType, svcType !== "rpc" ? `${service}.go` : "pb.go"), + })); + } + return all; + }, +} + + +const Generators = { + async api(apiFile) { + const serviceName = Paths.getServiceName(apiFile); + const outputDir = path.join(Paths.getOutputDir(serviceName), 'api'); + + await fs.mkdir(outputDir, { recursive: true }); + await run('goctl', [ + 'api', 'go', + '--api', apiFile, + '--dir', outputDir, + '--style', 'goZero' + ]); + }, + async rpc(protoFile) { + const serviceName = Paths.getServiceName(protoFile); + const outputDir = path.join(Paths.getOutputDir(serviceName), 'rpc'); + + await fs.mkdir(outputDir, { recursive: true }); + await run('goctl', [ + 'rpc', 'protoc', protoFile, + `--proto_path=${path.join(Paths.desc, "rpc",)}`, + `--go_out=${outputDir}`, + `--go-grpc_out=${outputDir}`, + `--zrpc_out=${outputDir}`, + '--style=goZero', + ]); + }, + async docker(servicePath) { + const dockerFiles = await glob("DockerFile", { cwd: __dirname, absolute: true }); + if (dockerFiles.length !== 0) { + fs.rm(dockerFiles[0], { force: true }); + } + await run('goctl', [ + "docker", "--go", path.relative(__dirname, servicePath) + ]) + } +}; + + +const GenerateConfig = { + api: { + getChoices: () => Paths.getAllApi(), + prompt: "Select an API description file", + generate: (path) => Generators.api(path), + }, + rpc: { + getChoices: () => Paths.getAllProto(), + prompt: "Select a proto file", + generate: (path) => Generators.rpc(path), + }, + docker: { + getChoices: () => Paths.getAllservice(), + prompt: "Select a service to generate Dockerfile", + generate: (path) => Generators.docker(path), + } +}; + + + +async function run(cmd, args, opts = {}) { + console.log(`>> ${cmd} ${args.join(' ')}`); + return execa(cmd, args, { + stdio: 'inherit', + ...opts + }); +} + + +async function searchSelector(chooses, message) { + const fuse = new Fuse(chooses, { + keys: ['title'], + threshold: 0.4, + }) + return search({ + message, + source: async (term) => { + if (!term) { + return chooses.map(s => ({ name: s.title, value: s.value })); + } + const result = fuse.search(term); + return result.map(s => ({ name: s.item.title, value: s.item.value })); + } + }) +} + + +async function generateHandle() { + const type = values.type; + if (!type || !GenerateConfig[type]) { + console.error("Please specify valid -t "); + return; + } + const config = GenerateConfig[type]; + + const input = values.server + ? (Array.isArray(values.server) ? values.server[0] : values.server) + : await searchSelector(await config.getChoices(), config.prompt); + + await config.generate(input); +} + +async function buildImage(imageName) { + await run("docker", ["build", "-t", imageName, "."]) +} + + +export const init = task({ + name: "init", + desc: "initialize the project", + run: async () => { + await run("go", ["install", "github.com/zeromicro/go-zero/tools/goctl@latest"]); + } +}) + + +export const tidy = task({ + name: "tidy", + desc: "tidy go.mod and go.sum", + run: async () => { run("go", ["mod", "tidy"]) }, +}) + + +export const build = task({ + name: "build", + desc: "build docker image", + run: async () => { + if (values.imageName) { + buildImage(values.imageName); + } else { + console.error("Please specify image name with -i "); + } + } +}) + + +export const gen = task({ + name: "gen", + desc: "generate API/RPC service code", + run: generateHandle, +}); + + diff --git a/backup/user-rpc.yaml.backup b/backup/user-rpc.yaml.backup index 13676e3..960391e 100644 --- a/backup/user-rpc.yaml.backup +++ b/backup/user-rpc.yaml.backup @@ -1,233 +1,233 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: user-rpc - namespace: juwan - labels: - app: user-rpc -spec: - replicas: 3 - revisionHistoryLimit: 5 - selector: - matchLabels: - app: user-rpc - template: - metadata: - labels: - app: user-rpc - spec: - serviceAccountName: find-endpoints - initContainers: # 等待数据库就绪的 Init Container 不影响资源使用但是影响调度策略(也可以忽略不计) - - name: wait-for-db - image: busybox:1.36 - command: - [ - "sh", - "-c", - 'until nc -z -v -w5 user-db-rw 5432; do echo "Waiting for database..."; sleep 2; done;', - ] - containers: - - name: user-rpc - image: user-rpc:v1 - ports: - - containerPort: 9001 - - containerPort: 4001 - env: - - name: DB_URI - valueFrom: - secretKeyRef: - name: user-db-app - key: uri - - name: REDIS_HOST - value: "user-redis-sentinel-sentinel.juwan:26379" - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: user-redis - key: password - readinessProbe: - tcpSocket: - port: 9001 - initialDelaySeconds: 5 - periodSeconds: 10 - livenessProbe: - tcpSocket: - port: 9001 - initialDelaySeconds: 15 - periodSeconds: 20 - resources: - requests: - cpu: 500m - memory: 512Mi - limits: - cpu: 1000m - memory: 1024Mi - volumeMounts: - - name: timezone - mountPath: /etc/localtime - volumes: - - name: timezone - hostPath: - path: /usr/share/zoneinfo/Asia/Shanghai - ---- -apiVersion: v1 -kind: Service -metadata: - name: user-rpc-svc - namespace: juwan - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "4001" - prometheus.io/path: "/metrics" -spec: - ports: - - name: rpc - port: 9001 - targetPort: 9001 - - name: metrics - port: 4001 - targetPort: 4001 - selector: - app: user-rpc - ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: user-rpc-hpa-c - namespace: juwan - labels: - app: user-rpc-hpa-c -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: user-rpc - minReplicas: 3 - maxReplicas: 10 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 80 - ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: user-rpc-hpa-m - namespace: juwan - labels: - app: user-rpc-hpa-m -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: user-rpc - minReplicas: 3 - maxReplicas: 10 - metrics: - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: 80 ---- -# Redis 主从复制 -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisReplication -metadata: - name: user-redis - namespace: juwan -spec: - clusterSize: 3 - kubernetesConfig: - image: quay.io/opstree/redis:v7.0.12 - imagePullPolicy: IfNotPresent - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - redisSecret: - name: user-redis - key: password - - redisExporter: - enabled: true - image: quay.io/opstree/redis-exporter:latest - imagePullPolicy: Always - podSecurityContext: - runAsUser: 1000 - fsGroup: 1000 - storage: - volumeClaimTemplate: - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - ---- -# Sentinel 监控 -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisSentinel -metadata: - name: user-redis-sentinel - namespace: juwan -spec: - clusterSize: 3 - kubernetesConfig: - image: quay.io/opstree/redis-sentinel:v7.0.12 - imagePullPolicy: IfNotPresent - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - podSecurityContext: - runAsUser: 1000 - fsGroup: 1000 - redisSentinelConfig: - redisReplicationName: user-redis - masterGroupName: mymaster - redisPort: "6379" - quorum: "2" - downAfterMilliseconds: "5000" - failoverTimeout: "10000" - parallelSyncs: "1" - ---- -# PostgreSQL 集群 -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - namespace: juwan - name: user-db -spec: - instances: 3 - backup: - barmanObjectStore: - destinationPath: s3://juwan-dev-pg-backups-zj/pg-data/ - endpointURL: https://cn-nb1.rains3.com - s3Credentials: - accessKeyId: - name: rc-creds - key: SOucqRaJr4OyfcIu - secretAccessKey: - name: rc-creds - key: tn2Agj9EowMwuPA9y7TdSL0AXKsMEz - wal: - compression: gzip - storage: - size: 1Gi - monitoring: - enablePodMonitor: true +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-rpc + namespace: juwan + labels: + app: user-rpc +spec: + replicas: 3 + revisionHistoryLimit: 5 + selector: + matchLabels: + app: user-rpc + template: + metadata: + labels: + app: user-rpc + spec: + serviceAccountName: find-endpoints + initContainers: # 等待数据库就绪的 Init Container 不影响资源使用但是影响调度策略(也可以忽略不计) + - name: wait-for-db + image: busybox:1.36 + command: + [ + "sh", + "-c", + 'until nc -z -v -w5 user-db-rw 5432; do echo "Waiting for database..."; sleep 2; done;', + ] + containers: + - name: user-rpc + image: user-rpc:v1 + ports: + - containerPort: 9001 + - containerPort: 4001 + env: + - name: DB_URI + valueFrom: + secretKeyRef: + name: user-db-app + key: uri + - name: REDIS_HOST + value: "user-redis-sentinel-sentinel.juwan:26379" + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: user-redis + key: password + readinessProbe: + tcpSocket: + port: 9001 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 9001 + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: 1000m + memory: 1024Mi + volumeMounts: + - name: timezone + mountPath: /etc/localtime + volumes: + - name: timezone + hostPath: + path: /usr/share/zoneinfo/Asia/Shanghai + +--- +apiVersion: v1 +kind: Service +metadata: + name: user-rpc-svc + namespace: juwan + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "4001" + prometheus.io/path: "/metrics" +spec: + ports: + - name: rpc + port: 9001 + targetPort: 9001 + - name: metrics + port: 4001 + targetPort: 4001 + selector: + app: user-rpc + +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: user-rpc-hpa-c + namespace: juwan + labels: + app: user-rpc-hpa-c +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: user-rpc + minReplicas: 3 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 80 + +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: user-rpc-hpa-m + namespace: juwan + labels: + app: user-rpc-hpa-m +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: user-rpc + minReplicas: 3 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 +--- +# Redis 主从复制 +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisReplication +metadata: + name: user-redis + namespace: juwan +spec: + clusterSize: 3 + kubernetesConfig: + image: quay.io/opstree/redis:v7.0.12 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + redisSecret: + name: user-redis + key: password + + redisExporter: + enabled: true + image: quay.io/opstree/redis-exporter:latest + imagePullPolicy: Always + podSecurityContext: + runAsUser: 1000 + fsGroup: 1000 + storage: + volumeClaimTemplate: + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + +--- +# Sentinel 监控 +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisSentinel +metadata: + name: user-redis-sentinel + namespace: juwan +spec: + clusterSize: 3 + kubernetesConfig: + image: quay.io/opstree/redis-sentinel:v7.0.12 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + podSecurityContext: + runAsUser: 1000 + fsGroup: 1000 + redisSentinelConfig: + redisReplicationName: user-redis + masterGroupName: mymaster + redisPort: "6379" + quorum: "2" + downAfterMilliseconds: "5000" + failoverTimeout: "10000" + parallelSyncs: "1" + +--- +# PostgreSQL 集群 +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + namespace: juwan + name: user-db +spec: + instances: 3 + backup: + barmanObjectStore: + destinationPath: s3://juwan-dev-pg-backups-zj/pg-data/ + endpointURL: https://cn-nb1.rains3.com + s3Credentials: + accessKeyId: + name: rc-creds + key: SOucqRaJr4OyfcIu + secretAccessKey: + name: rc-creds + key: tn2Agj9EowMwuPA9y7TdSL0AXKsMEz + wal: + compression: gzip + storage: + size: 1Gi + monitoring: + enablePodMonitor: true diff --git a/common/converter/README.md b/common/converter/README.md index 38fbe67..8a4dfb8 100644 --- a/common/converter/README.md +++ b/common/converter/README.md @@ -1,260 +1,260 @@ -# Converter - 通用结构体转换工具 - -利用 Go 反射机制,实现自动的 model 到 protobuf 结构体转换。 - -## 功能特性 - -✅ **自动字段映射** - 自动匹配同名字段并赋值 -✅ **智能类型转换** - 自动处理常见类型转换 -✅ **通用设计** - 支持任何 model 和 pb 结构体,无需手动编写 -✅ **灵活扩展** - 支持自定义类型转换规则 - -## 支持的类型转换 - -| 源类型 | 目标类型 | 说明 | -|-------|---------|------| -| `time.Time` | `int64` | 转换为 Unix 时间戳 | -| `sql.NullTime` | `int64` | 有效时自动转换,无效则为 0 | -| `sql.NullTime` | `time.Time` | 有效时自动转换,无效则为零值 | -| `sql.NullInt64` | `int64` | 有效时自动转换,无效则为 0 | -| `sql.NullString` | `string` | 有效时自动转换,无效则为空字符串 | -| `sql.NullBool` | `bool` | 有效时自动转换,无效则为 false | -| `int` | `int64` | 自动转换 | -| `int64` | `int` | 自动转换 | -| 相同类型 | 相同类型 | 直接复制 | - -## 核心函数 - -### 1. StructToStruct - 单个结构体转换 - -```go -func StructToStruct(src, dst interface{}) error -``` - -**参数:** -- `src` - 源结构体(可以是指针或值类型) -- `dst` - 目标结构体(必须是指针) - -**示例:** - -```go -import "app/common/converter" - -// 单个 models 转 pb -user, _ := m.FindOne(ctx, userId) -pbUser := &pb.Users{} -converter.StructToStruct(user, pbUser) - -// 或直接点对点转换 -pbUser := &pb.Users{} -_ = converter.StructToStruct(user, pbUser) -``` - -### 2. SliceToSlice - 切片转换 - -```go -func SliceToSlice(src interface{}, dstSliceType interface{}) (interface{}, error) -``` - -**参数:** -- `src` - 源切片 -- `dstSliceType` - 目标切片类型(用于推导元素类型) - -**示例:** - -```go -// 多个 models 转 pb -users := []*models.Users{user1, user2, user3} -pbUsersIface, _ := converter.SliceToSlice(users, []*pb.Users{}) -pbUsers := pbUsersIface.([]*pb.Users) -``` - -### 3. UserModelToPb - Users 专用转换(推荐) - -```go -func UserModelToPb(user *models.Users) *pb.Users -``` - -简化的 Users model 转 pb 的快捷函数。 - -**示例:** - -```go -user, _ := m.FindOne(ctx, userId) -pbUser := converter.UserModelToPb(user) -``` - -### 4. UserModelsToPb - Users 批量转换(推荐) - -```go -func UserModelsToPb(users []*models.Users) []*pb.Users -``` - -简化的批量转换快捷函数。 - -**示例:** - -```go -users, _ := m.FindAll(ctx) -pbUsers := converter.UserModelsToPb(users) -``` - -## 使用场景 - -### 场景 1:在 Logic 层直接转换 - -```go -package logic - -import ( - "context" - "app/common/converter" - "app/users/rpc/internal/models" - "app/users/rpc/pb" -) - -type GetUserByIdLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext -} - -func (l *GetUserByIdLogic) GetUserById(req *pb.GetUserByIdReq) (*pb.Users, error) { - // 查询数据库 - user, err := l.svcCtx.UsersModel.FindOne(l.ctx, req.UserId) - if err != nil { - return nil, err - } - - // 直接转换,无需手动赋值每个字段 - pbUser := converter.UserModelToPb(user) - - return pbUser, nil -} -``` - -### 场景 2:批量操作 - -```go -func (l *ListUsersLogic) ListUsers(req *pb.ListUsersReq) (*pb.ListUsersResp, error) { - users, err := l.svcCtx.UsersModel.FindAll(l.ctx) - if err != nil { - return nil, err - } - - // 批量转换 - pbUsers := converter.UserModelsToPb(users) - - return &pb.ListUsersResp{ - Users: pbUsers, - }, nil -} -``` - -### 场景 3:搜索/过滤结果 - -```go -func (l *SearchUsersLogic) SearchUsers(req *pb.SearchReq) (*pb.SearchResp, error) { - // 搜索数据库 - results, err := l.svcCtx.UsersModel.SearchByKeyword(l.ctx, req.Keyword) - if err != nil { - return nil, err - } - - pbUsers := converter.UserModelsToPb(results) - - return &pb.SearchResp{ - Results: pbUsers, - }, nil -} -``` - -## 处理特殊字段 - -### NULLable 字段 - -当源字段是 `sql.NullTime` 或其他 `sql.Null*` 类型时,转换器会自动处理: - -```go -// sql.NullTime -> int64(有效情况) -user.DeletedAt = sql.NullTime{ - Time: time.Now(), - Valid: true, -} -// 转换后 pb.Users.DeletedAt 会包含 Unix 时间戳 - -// sql.NullTime -> int64(无效情况) -user.DeletedAt = sql.NullTime{ - Valid: false, -} -// 转换后 pb.Users.DeletedAt 为 0 -``` - -### 时间戳字段 - -数据库中的 `time.Time` 字段会自动转换为 protobuf 中的 `int64` Unix 时间戳: - -```go -// Model -type Users struct { - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - DeletedAt sql.NullTime `db:"deleted_at"` -} - -// Protobuf -type Users struct { - CreatedAt int64 // 自动转换为 Unix 时间戳 - UpdatedAt int64 // 自动转换为 Unix 时间戳 - DeletedAt int64 // 有效时转换,无效时为 0 -} -``` - -## 扩展 - 添加自定义类型转换 - -如果需要支持新的类型转换,可以在 `generic.go` 的 `assignValue` 函数中添加: - -```go -// 处理自定义类型 MyType -> int32 的转换 -if srcType == reflect.TypeOf(MyType{}) && dstType.Kind() == reflect.Int32 { - mt := srcField.Interface().(MyType) - dstField.SetInt(int64(mt.SomeIntField)) - return nil -} -``` - -## 性能考虑 - -- 反射操作相对于直接赋值会有性能开销(通常很小) -- 如果需要转换大量数据(>10000 条),考虑性能测试 -- 对于热点代码路径,可以写针对性的转换函数 - -## 错误处理 - -```go -err := converter.StructToStruct(src, dst) -if err != nil { - log.Printf("转换失败: %v", err) - // 处理错误 -} -``` - -大多数字段级别的转换错误会被忽略(自动跳过),但结构化错误(如 dst 不是指针)会返回。 - -## 常见问题 - -**Q: 字段名必须完全相同吗?** -A: 是的,转换器通过反射按字段名匹配。如果 model 字段名是 `UserId`,pb 字段也必须是 `UserId`。 - -**Q: 如果某个字段转换失败怎么办?** -A: 单个字段的转换失败会被忽略,继续处理其他字段。确保其他字段正确设置。 - -**Q: 能否自定义字段映射规则(比如 `db_name` -> `pbName`)?** -A: 当前不支持。如果需要,应该在 protobuf 定义中使用与 model 相同的字段名。 - -**Q: 转换速度快吗?** -A: 反射会有性能开销,但对于大多数应用场景是可接受的。如果有极端性能要求,可以手写转换函数。 - -## 相关文件 - -- `generic.go` - 通用转换函数核心实现 -- `user_converter.go` - Users model 专用转换函数(示例) +# Converter - 通用结构体转换工具 + +利用 Go 反射机制,实现自动的 model 到 protobuf 结构体转换。 + +## 功能特性 + +✅ **自动字段映射** - 自动匹配同名字段并赋值 +✅ **智能类型转换** - 自动处理常见类型转换 +✅ **通用设计** - 支持任何 model 和 pb 结构体,无需手动编写 +✅ **灵活扩展** - 支持自定义类型转换规则 + +## 支持的类型转换 + +| 源类型 | 目标类型 | 说明 | +|-------|---------|------| +| `time.Time` | `int64` | 转换为 Unix 时间戳 | +| `sql.NullTime` | `int64` | 有效时自动转换,无效则为 0 | +| `sql.NullTime` | `time.Time` | 有效时自动转换,无效则为零值 | +| `sql.NullInt64` | `int64` | 有效时自动转换,无效则为 0 | +| `sql.NullString` | `string` | 有效时自动转换,无效则为空字符串 | +| `sql.NullBool` | `bool` | 有效时自动转换,无效则为 false | +| `int` | `int64` | 自动转换 | +| `int64` | `int` | 自动转换 | +| 相同类型 | 相同类型 | 直接复制 | + +## 核心函数 + +### 1. StructToStruct - 单个结构体转换 + +```go +func StructToStruct(src, dst interface{}) error +``` + +**参数:** +- `src` - 源结构体(可以是指针或值类型) +- `dst` - 目标结构体(必须是指针) + +**示例:** + +```go +import "app/common/converter" + +// 单个 models 转 pb +user, _ := m.FindOne(ctx, userId) +pbUser := &pb.Users{} +converter.StructToStruct(user, pbUser) + +// 或直接点对点转换 +pbUser := &pb.Users{} +_ = converter.StructToStruct(user, pbUser) +``` + +### 2. SliceToSlice - 切片转换 + +```go +func SliceToSlice(src interface{}, dstSliceType interface{}) (interface{}, error) +``` + +**参数:** +- `src` - 源切片 +- `dstSliceType` - 目标切片类型(用于推导元素类型) + +**示例:** + +```go +// 多个 models 转 pb +users := []*models.Users{user1, user2, user3} +pbUsersIface, _ := converter.SliceToSlice(users, []*pb.Users{}) +pbUsers := pbUsersIface.([]*pb.Users) +``` + +### 3. UserModelToPb - Users 专用转换(推荐) + +```go +func UserModelToPb(user *models.Users) *pb.Users +``` + +简化的 Users model 转 pb 的快捷函数。 + +**示例:** + +```go +user, _ := m.FindOne(ctx, userId) +pbUser := converter.UserModelToPb(user) +``` + +### 4. UserModelsToPb - Users 批量转换(推荐) + +```go +func UserModelsToPb(users []*models.Users) []*pb.Users +``` + +简化的批量转换快捷函数。 + +**示例:** + +```go +users, _ := m.FindAll(ctx) +pbUsers := converter.UserModelsToPb(users) +``` + +## 使用场景 + +### 场景 1:在 Logic 层直接转换 + +```go +package logic + +import ( + "context" + "app/common/converter" + "app/users/rpc/internal/models" + "app/users/rpc/pb" +) + +type GetUserByIdLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func (l *GetUserByIdLogic) GetUserById(req *pb.GetUserByIdReq) (*pb.Users, error) { + // 查询数据库 + user, err := l.svcCtx.UsersModel.FindOne(l.ctx, req.UserId) + if err != nil { + return nil, err + } + + // 直接转换,无需手动赋值每个字段 + pbUser := converter.UserModelToPb(user) + + return pbUser, nil +} +``` + +### 场景 2:批量操作 + +```go +func (l *ListUsersLogic) ListUsers(req *pb.ListUsersReq) (*pb.ListUsersResp, error) { + users, err := l.svcCtx.UsersModel.FindAll(l.ctx) + if err != nil { + return nil, err + } + + // 批量转换 + pbUsers := converter.UserModelsToPb(users) + + return &pb.ListUsersResp{ + Users: pbUsers, + }, nil +} +``` + +### 场景 3:搜索/过滤结果 + +```go +func (l *SearchUsersLogic) SearchUsers(req *pb.SearchReq) (*pb.SearchResp, error) { + // 搜索数据库 + results, err := l.svcCtx.UsersModel.SearchByKeyword(l.ctx, req.Keyword) + if err != nil { + return nil, err + } + + pbUsers := converter.UserModelsToPb(results) + + return &pb.SearchResp{ + Results: pbUsers, + }, nil +} +``` + +## 处理特殊字段 + +### NULLable 字段 + +当源字段是 `sql.NullTime` 或其他 `sql.Null*` 类型时,转换器会自动处理: + +```go +// sql.NullTime -> int64(有效情况) +user.DeletedAt = sql.NullTime{ + Time: time.Now(), + Valid: true, +} +// 转换后 pb.Users.DeletedAt 会包含 Unix 时间戳 + +// sql.NullTime -> int64(无效情况) +user.DeletedAt = sql.NullTime{ + Valid: false, +} +// 转换后 pb.Users.DeletedAt 为 0 +``` + +### 时间戳字段 + +数据库中的 `time.Time` 字段会自动转换为 protobuf 中的 `int64` Unix 时间戳: + +```go +// Model +type Users struct { + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + DeletedAt sql.NullTime `db:"deleted_at"` +} + +// Protobuf +type Users struct { + CreatedAt int64 // 自动转换为 Unix 时间戳 + UpdatedAt int64 // 自动转换为 Unix 时间戳 + DeletedAt int64 // 有效时转换,无效时为 0 +} +``` + +## 扩展 - 添加自定义类型转换 + +如果需要支持新的类型转换,可以在 `generic.go` 的 `assignValue` 函数中添加: + +```go +// 处理自定义类型 MyType -> int32 的转换 +if srcType == reflect.TypeOf(MyType{}) && dstType.Kind() == reflect.Int32 { + mt := srcField.Interface().(MyType) + dstField.SetInt(int64(mt.SomeIntField)) + return nil +} +``` + +## 性能考虑 + +- 反射操作相对于直接赋值会有性能开销(通常很小) +- 如果需要转换大量数据(>10000 条),考虑性能测试 +- 对于热点代码路径,可以写针对性的转换函数 + +## 错误处理 + +```go +err := converter.StructToStruct(src, dst) +if err != nil { + log.Printf("转换失败: %v", err) + // 处理错误 +} +``` + +大多数字段级别的转换错误会被忽略(自动跳过),但结构化错误(如 dst 不是指针)会返回。 + +## 常见问题 + +**Q: 字段名必须完全相同吗?** +A: 是的,转换器通过反射按字段名匹配。如果 model 字段名是 `UserId`,pb 字段也必须是 `UserId`。 + +**Q: 如果某个字段转换失败怎么办?** +A: 单个字段的转换失败会被忽略,继续处理其他字段。确保其他字段正确设置。 + +**Q: 能否自定义字段映射规则(比如 `db_name` -> `pbName`)?** +A: 当前不支持。如果需要,应该在 protobuf 定义中使用与 model 相同的字段名。 + +**Q: 转换速度快吗?** +A: 反射会有性能开销,但对于大多数应用场景是可接受的。如果有极端性能要求,可以手写转换函数。 + +## 相关文件 + +- `generic.go` - 通用转换函数核心实现 +- `user_converter.go` - Users model 专用转换函数(示例) diff --git a/deploy/certs/tls.crt b/deploy/certs/tls.crt index af4004d..9fb6f20 100644 --- a/deploy/certs/tls.crt +++ b/deploy/certs/tls.crt @@ -1,30 +1,30 @@ ------BEGIN CERTIFICATE----- -MIIFFTCCAv2gAwIBAgIUXj+1vyqKDhTsubwSmcHY61+YvmQwDQYJKoZIhvcNAQEL -BQAwGjEYMBYGA1UEAwwPYXBpLmp1d2FuLmxvY2FsMB4XDTI2MDIyMzExNTYxNloX -DTI3MDIyMzExNTYxNlowGjEYMBYGA1UEAwwPYXBpLmp1d2FuLmxvY2FsMIICIjAN -BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkn7Jw5f0awGFbGL3ZHEPJanZO9Yk -JDUklLF3kABiXqawSFpM6pXfKMa4VHE6/MfpREQeX2lkvtBseOf/vhC4DLACui8g -yslUObv77xGSXmIwjFcXZzLPQ/gEs2lxikxeoI4Su9qpsUQNzUD10rvWMx0iea8Z -47Z4RI6fIlA5xC5N4VfUFQdE/VN670HdiTZ7YAFIg9F/ZJQMH+hPVNSLgY6J0RdU -3gqKAkAvmCQZyQKWG1eRqKauw4CIvk6d7N+nOzmwDb6clueFj7Kx4h4IAFHCQthn -eXrf21uBCVwVjs64ilnTVwFfklr79euYRHPmRqR5eswbIGpDEFOaf1smu4hrkK9s -tQ8YWey8TICymBaXr1hI+WjSVEQFN8xPoVQwiKJRdu7lIosDjbH8V/ooKGMhCHgl -5C995L3sKsMyCMkw90viYNy2jUuSNu2X3eK+QJip2D2smfSM2tBsFtiXyEk+WeyY -cRDlwB7+6vvVwCHqz0+4lr0HHBEky43m3NgUtZoulfRwv4znGXcMqvxVUm4pwoBf -lo7zVuXh+cXrEzzCksQiCBzBM115itb3la8RX8A4bRUs38XG6Bz+Qfr6RQspppV1 -vNd5mUOyBYNeVfErf59PnFsdMI3kD0UgwpLkkGdSGdzDKykdt7vffNRpV8jOYuuO -LxH+2WlebCv1N90CAwEAAaNTMFEwHQYDVR0OBBYEFF80R0EZORGRXrZTVrAfaatK -eNi9MB8GA1UdIwQYMBaAFF80R0EZORGRXrZTVrAfaatKeNi9MA8GA1UdEwEB/wQF -MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAHFZUflyNOCJqV+RghOAaVFDc7wqtZJ1 -d2dpIs28kKd43Nd+xjSZLmSmVhcntQNwqC8AHIuJKKNDmM5BRnzls1ZO+OLc+YcC -kXzO2aBrNz8a0S0nYGzgR+CoTPvd61RGGHbqQNvZiroWsC4NaR+7NYPzsORNaN+1 -p/xqZygOYLOcD5tP5iNlgBugD+nPEHL0cylE0XpoZ059MIITdlvsrdPgHhFn9Nvv -McPZp4nzpJvyUmVjkbT7ZbKIJFrOQ6qJ9U2y55F4xuHzvnaAsOGnGx1tyBHtvkA1 -IIovrku4su3TmMsBs/6ikT8XSR20gcsDq3N2RcFtgU5LONsWvUL9CTp7P7lMlIfg -v1RelzXDE2mESlZEbzbFyVVGAoEPZA4t6kgBV4zObxxp4YmimqGWmVs3qQ/A6wbV -OO4rLYW7NZeJLLvsGOabVK+jyFCMyB3YOS6nZ9q48SaWCHlFTZveluP5n/8Y5LGc -ppjaZbsG2/apCqlown6jKT7hkP84eu3a+HyQ6ZXpCa6P9c9OZ8bVlP8dXi4mRuhU -lINwIKA0HbFAzwhyArMkLFWsw26ImusLZH1KUjHabzbfxnDgb9hwIlSGyPrcHcYY -lXTlThSXL0ERoqafQTE9tpPFXC+LCneytAKUgM2TZ1KhRlisA9Tb3i0X4y/yJba7 -T2Eqz8rRnaIe ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFFTCCAv2gAwIBAgIUXj+1vyqKDhTsubwSmcHY61+YvmQwDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPYXBpLmp1d2FuLmxvY2FsMB4XDTI2MDIyMzExNTYxNloX +DTI3MDIyMzExNTYxNlowGjEYMBYGA1UEAwwPYXBpLmp1d2FuLmxvY2FsMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkn7Jw5f0awGFbGL3ZHEPJanZO9Yk +JDUklLF3kABiXqawSFpM6pXfKMa4VHE6/MfpREQeX2lkvtBseOf/vhC4DLACui8g +yslUObv77xGSXmIwjFcXZzLPQ/gEs2lxikxeoI4Su9qpsUQNzUD10rvWMx0iea8Z +47Z4RI6fIlA5xC5N4VfUFQdE/VN670HdiTZ7YAFIg9F/ZJQMH+hPVNSLgY6J0RdU +3gqKAkAvmCQZyQKWG1eRqKauw4CIvk6d7N+nOzmwDb6clueFj7Kx4h4IAFHCQthn +eXrf21uBCVwVjs64ilnTVwFfklr79euYRHPmRqR5eswbIGpDEFOaf1smu4hrkK9s +tQ8YWey8TICymBaXr1hI+WjSVEQFN8xPoVQwiKJRdu7lIosDjbH8V/ooKGMhCHgl +5C995L3sKsMyCMkw90viYNy2jUuSNu2X3eK+QJip2D2smfSM2tBsFtiXyEk+WeyY +cRDlwB7+6vvVwCHqz0+4lr0HHBEky43m3NgUtZoulfRwv4znGXcMqvxVUm4pwoBf +lo7zVuXh+cXrEzzCksQiCBzBM115itb3la8RX8A4bRUs38XG6Bz+Qfr6RQspppV1 +vNd5mUOyBYNeVfErf59PnFsdMI3kD0UgwpLkkGdSGdzDKykdt7vffNRpV8jOYuuO +LxH+2WlebCv1N90CAwEAAaNTMFEwHQYDVR0OBBYEFF80R0EZORGRXrZTVrAfaatK +eNi9MB8GA1UdIwQYMBaAFF80R0EZORGRXrZTVrAfaatKeNi9MA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAHFZUflyNOCJqV+RghOAaVFDc7wqtZJ1 +d2dpIs28kKd43Nd+xjSZLmSmVhcntQNwqC8AHIuJKKNDmM5BRnzls1ZO+OLc+YcC +kXzO2aBrNz8a0S0nYGzgR+CoTPvd61RGGHbqQNvZiroWsC4NaR+7NYPzsORNaN+1 +p/xqZygOYLOcD5tP5iNlgBugD+nPEHL0cylE0XpoZ059MIITdlvsrdPgHhFn9Nvv +McPZp4nzpJvyUmVjkbT7ZbKIJFrOQ6qJ9U2y55F4xuHzvnaAsOGnGx1tyBHtvkA1 +IIovrku4su3TmMsBs/6ikT8XSR20gcsDq3N2RcFtgU5LONsWvUL9CTp7P7lMlIfg +v1RelzXDE2mESlZEbzbFyVVGAoEPZA4t6kgBV4zObxxp4YmimqGWmVs3qQ/A6wbV +OO4rLYW7NZeJLLvsGOabVK+jyFCMyB3YOS6nZ9q48SaWCHlFTZveluP5n/8Y5LGc +ppjaZbsG2/apCqlown6jKT7hkP84eu3a+HyQ6ZXpCa6P9c9OZ8bVlP8dXi4mRuhU +lINwIKA0HbFAzwhyArMkLFWsw26ImusLZH1KUjHabzbfxnDgb9hwIlSGyPrcHcYY +lXTlThSXL0ERoqafQTE9tpPFXC+LCneytAKUgM2TZ1KhRlisA9Tb3i0X4y/yJba7 +T2Eqz8rRnaIe +-----END CERTIFICATE----- diff --git a/deploy/certs/tls.key b/deploy/certs/tls.key index 35d20b8..3e2eeb8 100644 --- a/deploy/certs/tls.key +++ b/deploy/certs/tls.key @@ -1,52 +1,52 @@ ------BEGIN PRIVATE KEY----- -MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCSfsnDl/RrAYVs -YvdkcQ8lqdk71iQkNSSUsXeQAGJeprBIWkzqld8oxrhUcTr8x+lERB5faWS+0Gx4 -5/++ELgMsAK6LyDKyVQ5u/vvEZJeYjCMVxdnMs9D+ASzaXGKTF6gjhK72qmxRA3N -QPXSu9YzHSJ5rxnjtnhEjp8iUDnELk3hV9QVB0T9U3rvQd2JNntgAUiD0X9klAwf -6E9U1IuBjonRF1TeCooCQC+YJBnJApYbV5Gopq7DgIi+Tp3s36c7ObANvpyW54WP -srHiHggAUcJC2Gd5et/bW4EJXBWOzriKWdNXAV+SWvv165hEc+ZGpHl6zBsgakMQ -U5p/Wya7iGuQr2y1DxhZ7LxMgLKYFpevWEj5aNJURAU3zE+hVDCIolF27uUiiwON -sfxX+igoYyEIeCXkL33kvewqwzIIyTD3S+Jg3LaNS5I27Zfd4r5AmKnYPayZ9Iza -0GwW2JfIST5Z7JhxEOXAHv7q+9XAIerPT7iWvQccESTLjebc2BS1mi6V9HC/jOcZ -dwyq/FVSbinCgF+WjvNW5eH5xesTPMKSxCIIHMEzXXmK1veVrxFfwDhtFSzfxcbo -HP5B+vpFCymmlXW813mZQ7IFg15V8St/n0+cWx0wjeQPRSDCkuSQZ1IZ3MMrKR23 -u9981GlXyM5i644vEf7ZaV5sK/U33QIDAQABAoICAA34ohDxm8mdxEYFPT9ayf1H -UNS0VE+QsuusbjDxXHBW+N55oDbKMtV+eENzZhMIFM7iKTxjvow1L/cq9xi/GvJ4 -0dXEW14Dq/DypPEUra8rMaKcxrpcnehHTdl3f7DXHjo1OoOoc8EYcrGF1bvylpfa -2jgdMzykoR02teYNnSjA2sQYPn1/6zw2uzV4xGJK7CLIlIwfzYS/2tUrMG+wcpqZ -R7sFfN5NRoK28OMTZFMnmD3E0Psy5F14U3JE6KpX3SjYlFoHOQNqUrJU8kKUpyIy -qfJ6lYnAJnvS4wBLxDGRtQda0D1Ov/jjDP8T6Dp1DDvmAUDtGNQzVjCHLKejP5MD -ltUjTDiFqSXzcRmEV2Jq8y/DjqWieM3BGGl77W6W8eksYqLSo2Ik4fJLqoTm5TSw -QY6d8/9gZAP+0E64MWnu0cxpMEXikPPrcjhcTFASxNBoxVhxKseRt+tgkgP4krPu -hG2WsWY7n5B0iuO5Dxi0yttT5LfpKcrmRlQXqs0Jdn6nxA7us62WgegxBCXPHCpE -rMHlsbrmJECkvnQ11P7eRnpD56b5uD7Kg4uMcUVdY+EKESjm2SwK0FrSBJRvQ/mg -JKC7rf2tx7XB3tiKPrmygtLzwyU18+drCMI7fpcrf7wgwyuSdH3klkqnjF/xchQ9 -RkT3ZDR6mpxhv/ytoXrhAoIBAQDISEKgj/Z+2bNLhryvRfQACzvLHVp59oyI5Xa6 -MxLIVtozpq05wJxUgY4iPVXLx1Vm9/osHhXQtsFwMQTG2RI0tcz4N7YXrH4acmlm -ErdoORtcRX15mEVAl7Mwac4LVyllOZ1D9woKboHDmlBO2L8FUXy8RiLdUT0jgK7k -ShWb35twbqwQDLezLEiMnxKFCarVFVBTxVn2bhRA5jcPU+9S2oK9Qx/Mei7QlKKE -uTXLOTtNGSvY/7h0dExzS8nXwRDvsVCrWCT5pca1KfmR/JPOPbM6I/vwziSIqMNk -GfZWe5IlsQRtyZ49DpA+eDQxzMhxjZhWQ6JR5iQFWUCtXKIhAoIBAQC7P+pPEf9l -KOhdPJu6p9NPQu6+hMTi5rTyCDsH5VggvoLKDZTJ1BSqWtW1K09UafRWP5vOxm7u -fBYcnqu0W5RSUuoQTZiu03ZhYLBV5vbR+Icx0Hc2BDl8eEIyevyqsmm8+w5i28Ar -knep4sP7/n+q2EAK1B2ZlNXXz6f47CMQMkVvZp3FR0F7R6yJoS32tTL5wDOxuOFG -LYQOG/yI5JWXwBXov83zrpc+C7kl4gV3xAOk7fZ0exoRdmuvdLS96Ans85L7J8RW -ELSfhmGahM+SQ1oJMcV/wYqF2qeLL2F8DZbjR5izLZgkNz4a/VMl/A6YHtuTBXAY -+5PXXUOX+9Y9AoIBACI+II4dLxLPG9WM6tO4zRf407dNhHuXyL1bJip9svdnyhTM -qY9XPCNCp095VyLpKNPbD/3dAvPVW0tYRi3NTUyPzMSfmdWAW2sgJp8aEhuSr/fd -ta9Fdomtpihf3qeXtm8lI5tMMH5KGIud5Z8ldbtuDDqQb0ORsTdRuBU2CW3GFGhr -s6Vm1z2eE6VfSSZP2dJmu34nHtOATJwwADfxrNhonbPINzaZqUlmMEcq92SQm2/6 -HsISLrJSdAO+cHsf+kpQ8a7p+iBo1ImC7LWmDotTh0IohtnMFPj8ibOisLhmlj01 -f8FZmGFuDQFxQdNF5PttLx+InscL5xq3ANTjIqECggEADpdtd9nsMALfEJzveb0o -P0308s2/1fqqcQ3pI7Vgh7Sw1nP2ez/WmGvZqXOFjAtxqeLtDlDyRg1PX82Rjc1x -InUpnjmdw0nhOLdjJl6IL1aRmnUnRQNRQ3zPk8V3uQmMKdjahyOetwaD4q40HYf4 -hOSzIOTkpZoui9G3wjMMjG+Ob57sfnoOBUBRlqwDu+zk2wd6P8grbd+QIdVWeYhu -i9PBIVEJCIs7Z+9b7zLMwEd7DTgp82vAXUoAHD0Y9I+HbnqQope3ugk1OhUrt/HP -hxNOidbiEBGR7NpcIgGAND2O24kxwgy0hWX0pf/FofkhXgNRkwRidt/r5mVzJf3O -9QKCAQAcPXczJY1gynUA8uD/1ODmjpDjWAk0EKBEWY5X2oULv2+xGMNTbT8pwE3f -1rszdtF3ckDPoBn7XS9OJwHnVHfXZNJHBtq9utLu0ccE+29HRG0pLCzATsvtoBWi -MEwZ2mPqhVpktfqEnL27l/QHkP7dNOyh+halVCHMfy1aNMY6hsKrOcmVmYHVARX0 -Np2sG9zQszE0+t2mf8Pfd7cEvVuSTIfYZnW+77+PaVkICXXX0rrvwXVh/DVXwmWH -kYbDIdiNs9NEFwCmIvzLVsCp0qGUuq9txYo/ML5PMzJhN6X3U+rV42GkkT7KxwH2 -Izss0+mp4ijKEFQuCGCkxjFmxUEq ------END PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCSfsnDl/RrAYVs +YvdkcQ8lqdk71iQkNSSUsXeQAGJeprBIWkzqld8oxrhUcTr8x+lERB5faWS+0Gx4 +5/++ELgMsAK6LyDKyVQ5u/vvEZJeYjCMVxdnMs9D+ASzaXGKTF6gjhK72qmxRA3N +QPXSu9YzHSJ5rxnjtnhEjp8iUDnELk3hV9QVB0T9U3rvQd2JNntgAUiD0X9klAwf +6E9U1IuBjonRF1TeCooCQC+YJBnJApYbV5Gopq7DgIi+Tp3s36c7ObANvpyW54WP +srHiHggAUcJC2Gd5et/bW4EJXBWOzriKWdNXAV+SWvv165hEc+ZGpHl6zBsgakMQ +U5p/Wya7iGuQr2y1DxhZ7LxMgLKYFpevWEj5aNJURAU3zE+hVDCIolF27uUiiwON +sfxX+igoYyEIeCXkL33kvewqwzIIyTD3S+Jg3LaNS5I27Zfd4r5AmKnYPayZ9Iza +0GwW2JfIST5Z7JhxEOXAHv7q+9XAIerPT7iWvQccESTLjebc2BS1mi6V9HC/jOcZ +dwyq/FVSbinCgF+WjvNW5eH5xesTPMKSxCIIHMEzXXmK1veVrxFfwDhtFSzfxcbo +HP5B+vpFCymmlXW813mZQ7IFg15V8St/n0+cWx0wjeQPRSDCkuSQZ1IZ3MMrKR23 +u9981GlXyM5i644vEf7ZaV5sK/U33QIDAQABAoICAA34ohDxm8mdxEYFPT9ayf1H +UNS0VE+QsuusbjDxXHBW+N55oDbKMtV+eENzZhMIFM7iKTxjvow1L/cq9xi/GvJ4 +0dXEW14Dq/DypPEUra8rMaKcxrpcnehHTdl3f7DXHjo1OoOoc8EYcrGF1bvylpfa +2jgdMzykoR02teYNnSjA2sQYPn1/6zw2uzV4xGJK7CLIlIwfzYS/2tUrMG+wcpqZ +R7sFfN5NRoK28OMTZFMnmD3E0Psy5F14U3JE6KpX3SjYlFoHOQNqUrJU8kKUpyIy +qfJ6lYnAJnvS4wBLxDGRtQda0D1Ov/jjDP8T6Dp1DDvmAUDtGNQzVjCHLKejP5MD +ltUjTDiFqSXzcRmEV2Jq8y/DjqWieM3BGGl77W6W8eksYqLSo2Ik4fJLqoTm5TSw +QY6d8/9gZAP+0E64MWnu0cxpMEXikPPrcjhcTFASxNBoxVhxKseRt+tgkgP4krPu +hG2WsWY7n5B0iuO5Dxi0yttT5LfpKcrmRlQXqs0Jdn6nxA7us62WgegxBCXPHCpE +rMHlsbrmJECkvnQ11P7eRnpD56b5uD7Kg4uMcUVdY+EKESjm2SwK0FrSBJRvQ/mg +JKC7rf2tx7XB3tiKPrmygtLzwyU18+drCMI7fpcrf7wgwyuSdH3klkqnjF/xchQ9 +RkT3ZDR6mpxhv/ytoXrhAoIBAQDISEKgj/Z+2bNLhryvRfQACzvLHVp59oyI5Xa6 +MxLIVtozpq05wJxUgY4iPVXLx1Vm9/osHhXQtsFwMQTG2RI0tcz4N7YXrH4acmlm +ErdoORtcRX15mEVAl7Mwac4LVyllOZ1D9woKboHDmlBO2L8FUXy8RiLdUT0jgK7k +ShWb35twbqwQDLezLEiMnxKFCarVFVBTxVn2bhRA5jcPU+9S2oK9Qx/Mei7QlKKE +uTXLOTtNGSvY/7h0dExzS8nXwRDvsVCrWCT5pca1KfmR/JPOPbM6I/vwziSIqMNk +GfZWe5IlsQRtyZ49DpA+eDQxzMhxjZhWQ6JR5iQFWUCtXKIhAoIBAQC7P+pPEf9l +KOhdPJu6p9NPQu6+hMTi5rTyCDsH5VggvoLKDZTJ1BSqWtW1K09UafRWP5vOxm7u +fBYcnqu0W5RSUuoQTZiu03ZhYLBV5vbR+Icx0Hc2BDl8eEIyevyqsmm8+w5i28Ar +knep4sP7/n+q2EAK1B2ZlNXXz6f47CMQMkVvZp3FR0F7R6yJoS32tTL5wDOxuOFG +LYQOG/yI5JWXwBXov83zrpc+C7kl4gV3xAOk7fZ0exoRdmuvdLS96Ans85L7J8RW +ELSfhmGahM+SQ1oJMcV/wYqF2qeLL2F8DZbjR5izLZgkNz4a/VMl/A6YHtuTBXAY ++5PXXUOX+9Y9AoIBACI+II4dLxLPG9WM6tO4zRf407dNhHuXyL1bJip9svdnyhTM +qY9XPCNCp095VyLpKNPbD/3dAvPVW0tYRi3NTUyPzMSfmdWAW2sgJp8aEhuSr/fd +ta9Fdomtpihf3qeXtm8lI5tMMH5KGIud5Z8ldbtuDDqQb0ORsTdRuBU2CW3GFGhr +s6Vm1z2eE6VfSSZP2dJmu34nHtOATJwwADfxrNhonbPINzaZqUlmMEcq92SQm2/6 +HsISLrJSdAO+cHsf+kpQ8a7p+iBo1ImC7LWmDotTh0IohtnMFPj8ibOisLhmlj01 +f8FZmGFuDQFxQdNF5PttLx+InscL5xq3ANTjIqECggEADpdtd9nsMALfEJzveb0o +P0308s2/1fqqcQ3pI7Vgh7Sw1nP2ez/WmGvZqXOFjAtxqeLtDlDyRg1PX82Rjc1x +InUpnjmdw0nhOLdjJl6IL1aRmnUnRQNRQ3zPk8V3uQmMKdjahyOetwaD4q40HYf4 +hOSzIOTkpZoui9G3wjMMjG+Ob57sfnoOBUBRlqwDu+zk2wd6P8grbd+QIdVWeYhu +i9PBIVEJCIs7Z+9b7zLMwEd7DTgp82vAXUoAHD0Y9I+HbnqQope3ugk1OhUrt/HP +hxNOidbiEBGR7NpcIgGAND2O24kxwgy0hWX0pf/FofkhXgNRkwRidt/r5mVzJf3O +9QKCAQAcPXczJY1gynUA8uD/1ODmjpDjWAk0EKBEWY5X2oULv2+xGMNTbT8pwE3f +1rszdtF3ckDPoBn7XS9OJwHnVHfXZNJHBtq9utLu0ccE+29HRG0pLCzATsvtoBWi +MEwZ2mPqhVpktfqEnL27l/QHkP7dNOyh+halVCHMfy1aNMY6hsKrOcmVmYHVARX0 +Np2sG9zQszE0+t2mf8Pfd7cEvVuSTIfYZnW+77+PaVkICXXX0rrvwXVh/DVXwmWH +kYbDIdiNs9NEFwCmIvzLVsCp0qGUuq9txYo/ML5PMzJhN6X3U+rV42GkkT7KxwH2 +Izss0+mp4ijKEFQuCGCkxjFmxUEq +-----END PRIVATE KEY----- diff --git a/deploy/dev/docker-compose.yml b/deploy/dev/docker-compose.yml index 7f430bd..65d7a88 100644 --- a/deploy/dev/docker-compose.yml +++ b/deploy/dev/docker-compose.yml @@ -35,6 +35,15 @@ services: timeout: 3s retries: 10 + rl-redis: + image: redis:${REDIS_VERSION:-8} + container_name: ${REDIS_CONTAINER_NAME:-rl-redis-dev-server} + profiles: + - infra + ports: + - "6380:6379" + restart: unless-stopped + kafka: image: apache/kafka:4.0.1 container_name: juwan-kafka @@ -77,40 +86,42 @@ services: condition: service_started envoy-gateway: - image: envoyproxy/envoy:v1.31-latest - container_name: juwan-envoy-gateway - restart: unless-stopped - command: - - /usr/local/bin/envoy - - -c - - /etc/envoy/envoy.yaml - - --log-level - - info + build: + context: ../deploy/dev/envoy + image: envoy-gateway:latest + container_name: ${ENVOY_GATEWAY_CONTAINER_NAME:-envoy-gateway-dev-server} ports: - - "18080:8080" - volumes: - - ./envoy.yaml:/etc/envoy/envoy.yaml:ro + - "8080:8080" + - "9901:9901" depends_on: authz-adapter: condition: service_started - users-api: - condition: service_started - player-api: - condition: service_started - game-api: - condition: service_started - shop-api: - condition: service_started - order-api: - condition: service_started - wallet-api: - condition: service_started - community-api: - condition: service_started - objectstory-api: + required: false + user-api: condition: service_started + required: false email-api: condition: service_started + required: false + restart: unless-stopped + + ratelimit: + image: ratelimit:latest + container_name: rl-service + restart: unless-stopped + environment: + - REDIS_SOCKET_TYPE=tcp + - REDIS_URL=rl-redis:6379 + - USE_STATSD=false + - RUNTIME_ROOT=/data + - RUNTIME_SUBDIRECTORY=ratelimit + - RUNTIME_WATCH_ROOT=true # 热重载 + - LOG_LEVEL=debug + volumes: + - ./rls/ratelimit.yaml:/data/ratelimit/config/ratelimit.yaml:ro + ports: + - "8081:8081" + - "6070:6070" # ==================== RPC 层 ==================== user-rpc: diff --git a/deploy/dev/envoy/envoy.yaml b/deploy/dev/envoy/envoy.yaml new file mode 100644 index 0000000..48f0c4e --- /dev/null +++ b/deploy/dev/envoy/envoy.yaml @@ -0,0 +1,626 @@ +static_resources: + listeners: + - name: ingress_http + address: + socket_address: + address: 0.0.0.0 + port_value: 8080 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: AUTO + generate_request_id: true + use_remote_address: true + xff_num_trusted_hops: 1 + route_config: + name: local_route + virtual_hosts: + - name: juwan_services + domains: [ "*" ] + routes: + - match: + path: /healthz + direct_response: + status: 200 + body: + inline_string: ok + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + path: /api/v1/auth/login + route: + cluster: user_api_cluster + timeout: 30s + rate_limits: + - actions: + - generic_key: + descriptor_value: login + - remote_address: {} + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + path: /api/v1/auth/register + route: + cluster: user_api_cluster + timeout: 30s + rate_limits: + - actions: + - generic_key: + descriptor_value: register + - generic_key: + descriptor_key: "period" + descriptor_value: "minute" + - remote_address: {} + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + path: /api/v1/auth/forgot-password + route: + cluster: user_api_cluster + timeout: 30s + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + path: /api/v1/auth/reset-password + route: + cluster: user_api_cluster + timeout: 30s + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + path: /api/v1/auth/forgot-password/send + route: + cluster: email_api_cluster + timeout: 30s + rate_limits: + - actions: + - generic_key: + descriptor_value: forgot_password_send + - generic_key: + descriptor_key: "period" + descriptor_value: "minute" + - remote_address: {} + - actions: + - generic_key: + descriptor_value: forgot_password_send + - generic_key: + descriptor_key: "period" + descriptor_value: "hour" + - remote_address: {} + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + prefix: /api/users + route: + cluster: user_api_cluster + timeout: 30s + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + prefix: /api/v1/shop + route: + cluster: shop_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/player + route: + cluster: player_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/games + route: + cluster: game_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/games + headers: + - name: :method + exact_match: GET + route: + cluster: game_api_cluster + timeout: 30s + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + path: /api/v1/email/verification-code/send + route: + cluster: email_api_cluster + timeout: 30s + rate_limits: + - actions: + - generic_key: + descriptor_value: verify_code_send + - generic_key: + descriptor_key: "period" + descriptor_value: "minute" + - remote_address: {} + - actions: + - generic_key: + descriptor_value: verify_code_send + - generic_key: + descriptor_key: "period" + descriptor_value: "hour" + - remote_address: {} + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + prefix: /api/v1/wallet + route: + cluster: wallet_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/players + route: + cluster: player_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/orders + route: + cluster: order_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/email + route: + cluster: email_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/auth + route: + cluster: user_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/upload + route: + cluster: objectstory_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/files + route: + cluster: objectstory_api_cluster + timeout: 30s + typed_per_filter_config: + envoy.filters.http.ext_authz: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute + disabled: true + + - match: + prefix: /api/email + route: + cluster: email_api_cluster + timeout: 30s + + - match: + prefix: /api/v1/game + route: + cluster: game_api_cluster + timeout: 30s + + - match: + prefix: /api/v1 + route: + cluster: user_api_cluster + timeout: 30s + + - match: + prefix: / + direct_response: + status: 404 + body: + inline_string: gateway route not found + access_log: + - name: envoy.access_loggers.stdout + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + log_format: + json_format: + start_time: "%START_TIME%" + method: "%REQ(:METHOD)%" + path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%" + protocol: "%PROTOCOL%" + authority: "%REQ(:AUTHORITY)%" + user_agent: "%REQ(USER-AGENT)%" + request_id: "%REQ(X-REQUEST-ID)%" + response_code: "%RESPONSE_CODE%" + response_flags: "%RESPONSE_FLAGS%" + bytes_received: "%BYTES_RECEIVED%" + bytes_sent: "%BYTES_SENT%" + duration_ms: "%DURATION%" + upstream_cluster: "%UPSTREAM_CLUSTER%" + upstream_host: "%UPSTREAM_HOST%" + upstream_service_time_ms: "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%" + route_name: "%ROUTE_NAME%" + http_filters: + - name: envoy.filters.http.lua + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua + inline_code: | + local TOKEN_HEADER = "xsrf-token" + local TOKEN_COOKIE = "__Host-XSRF-TOKEN" + local GUARD_COOKIE = "__Host-XSRF-GUARD" + + local seeded = false + + local function seed_random() + if seeded then + return + end + seeded = true + math.randomseed(os.time()) + end + + local function split_cookie(header) + local out = {} + if not header then + return out + end + for pair in string.gmatch(header, "([^;]+)") do + local key, value = string.match(pair, "^%s*([^=]+)=?(.*)$") + if key ~= nil and value ~= nil then + out[string.lower(key)] = value + end + end + return out + end + + local function is_safe_method(method) + return method == "GET" or method == "HEAD" or method == "OPTIONS" + end + + local function build_token(request_id) + seed_random() + local rnd = tostring(math.random(100000, 999999)) + local rid = request_id or "rid" + return tostring(os.time()) .. "-" .. rid .. "-" .. rnd + end + + function envoy_on_request(request_handle) + local headers = request_handle:headers() + local method = headers:get(":method") + + local cookie_header = headers:get("cookie") + local cookies = split_cookie(cookie_header) + local token_cookie = cookies[string.lower(TOKEN_COOKIE)] + local guard_cookie = cookies[string.lower(GUARD_COOKIE)] + + request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_token_cookie", token_cookie == nil or token_cookie == "") + request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_guard_cookie", guard_cookie == nil or guard_cookie == "") + + if token_cookie == nil or token_cookie == "" then + token_cookie = build_token(headers:get("x-request-id")) + request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", token_cookie) + else + request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", token_cookie) + end + + if guard_cookie == nil or guard_cookie == "" then + guard_cookie = build_token(headers:get("x-request-id")) + request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", guard_cookie) + else + request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", guard_cookie) + end + + if is_safe_method(method) then + return + end + + local token_header = headers:get(TOKEN_HEADER) + + if token_header == nil or token_header == "" then + request_handle:respond( + {[":status"] = "403", ["content-type"] = "application/json"}, + '{"code":403,"message":"missing XSRF-TOKEN header"}' + ) + return + end + + if token_cookie == nil or token_cookie == "" or guard_cookie == nil or guard_cookie == "" then + request_handle:respond( + {[":status"] = "403", ["content-type"] = "application/json"}, + '{"code":403,"message":"missing csrf cookies"}' + ) + return + end + + if token_header ~= token_cookie then + request_handle:respond( + {[":status"] = "403", ["content-type"] = "application/json"}, + '{"code":403,"message":"xsrf token mismatch"}' + ) + return + end + end + + function envoy_on_response(response_handle) + local metadata = response_handle:streamInfo():dynamicMetadata():get("csrf") + if metadata == nil then + return + end + + local token_value = metadata["token_value"] + local guard_value = metadata["guard_value"] + + if metadata["need_set_token_cookie"] == true and token_value ~= nil and token_value ~= "" then + response_handle:headers():add( + "set-cookie", + TOKEN_COOKIE .. "=" .. token_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure" + ) + end + + if metadata["need_set_guard_cookie"] == true and guard_value ~= nil and guard_value ~= "" then + response_handle:headers():add( + "set-cookie", + GUARD_COOKIE .. "=" .. guard_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure; HttpOnly" + ) + end + end + + - name: envoy.filters.http.jwt_authn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + providers: + juwan_user_jwt: + issuer: juwan-user-rpc + from_cookies: + - JToken + local_jwks: + inline_string: '{"keys":[{"kty":"oct","k":"TUdVeU1XRTNaRGhqTVRRNVpEZzFNV1ZpT1dVME1HTTNPVEUyTldWa1lUQmxPVEU1WldSa1pEVTFZall6T0dKak9XUmlOek0wTlRjNE5ESXlNamxrWlE","alg":"HS256","use":"sig","kid":"juwan-hs256-1"}]}' + forward: false + claim_to_headers: + - header_name: x-auth-user-id + claim_name: UserId + - header_name: x-auth-is-admin + claim_name: IsAdmin + rules: + - match: + path: /healthz + - match: + prefix: /api/v1 + headers: + - name: :method + exact_match: OPTIONS + - match: + path: /api/v1/auth/login + - match: + path: /api/v1/auth/register + - match: + path: /api/v1/auth/forgot-password + - match: + path: /api/v1/auth/reset-password + - match: + path: /api/v1/auth/forgot-password/send + - match: + path: /api/v1/email/verification-code/send + - match: + prefix: /api/v1 + requires: + provider_name: juwan_user_jwt + - match: + prefix: /api/users + requires: + provider_name: juwan_user_jwt + - match: + prefix: /api/email + requires: + provider_name: juwan_user_jwt + + - name: envoy.filters.http.ext_authz + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz + transport_api_version: V3 + failure_mode_allow: false + with_request_body: + max_request_bytes: 8192 + allow_partial_message: true + grpc_service: + envoy_grpc: + cluster_name: authz_adapter_cluster + timeout: 0.5s + + # RLS 全局过滤器 + - name: envoy.filters.http.ratelimit + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: api + failure_mode_deny: false + rate_limited_as_resource_exhausted: true + enable_x_ratelimit_headers: DRAFT_VERSION_03 + rate_limit_service: + transport_api_version: V3 + grpc_service: + envoy_grpc: + cluster_name: ratelimit_cluster + timeout: 0.2s + + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + - name: user_api_cluster + connect_timeout: 2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: user_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: user-api + port_value: 8888 + + - name: email_api_cluster + connect_timeout: 2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: email_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: email-api + port_value: 8888 + + - name: shop_api_cluster + connect_timeout: 2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: shop_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: shop-api + port_value: 8888 + + - name: game_api_cluster + connect_timeout: 2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: game_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: game-api + port_value: 8888 + + - name: objectstory_api_cluster + connect_timeout: 2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: objectstory_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: objectstory-api + port_value: 8888 + + - name: wallet_api_cluster + connect_timeout: 2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: wallet_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: wallet-api + port_value: 8888 + + - name: order_api_cluster + connect_timeout: 2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: order_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: order-api + port_value: 8888 + + - name: player_api_cluster + connect_timeout: 2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: player_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: player-api + port_value: 8888 + + - name: authz_adapter_cluster + connect_timeout: 0.5s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + http2_protocol_options: { } + load_assignment: + cluster_name: authz_adapter_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: authz-adapter + port_value: 9002 + + # RLS 集群 + - name: ratelimit_cluster + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + http2_protocol_options: {} + load_assignment: + cluster_name: ratelimit_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: ratelimit # RLS 地址 + port_value: 8081 # RLS gRPC 端口 + +admin: + access_log_path: /tmp/admin_access.log + address: + socket_address: + address: 0.0.0.0 + port_value: 9901 diff --git a/deploy/dev/rls/ratelimit.yaml b/deploy/dev/rls/ratelimit.yaml new file mode 100644 index 0000000..8ef2fbe --- /dev/null +++ b/deploy/dev/rls/ratelimit.yaml @@ -0,0 +1,33 @@ +domain: api +descriptors: + - key: generic_key + value: login + descriptors: + - key: remote_address + rate_limit: + unit: MINUTE + requests_per_unit: 10 + + - key: generic_key + value: register + descriptors: + - key: remote_address + rate_limit: + unit: MINUTE + requests_per_unit: 5 + + - key: generic_key + value: forgot_password_send + descriptors: + - key: remote_address + rate_limit: + unit: MINUTE + requests_per_unit: 3 + + - key: generic_key + value: verify_code_send + descriptors: + - key: remote_address + rate_limit: + unit: MINUTE + requests_per_unit: 3 diff --git a/deploy/k8s/monitoring/00-namespace.yaml b/deploy/k8s/monitoring/00-namespace.yaml index 0090389..d325236 100644 --- a/deploy/k8s/monitoring/00-namespace.yaml +++ b/deploy/k8s/monitoring/00-namespace.yaml @@ -1,4 +1,4 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: monitoring +apiVersion: v1 +kind: Namespace +metadata: + name: monitoring diff --git a/deploy/k8s/monitoring/grafana.yaml b/deploy/k8s/monitoring/grafana.yaml index 45df5ca..66bc0b3 100644 --- a/deploy/k8s/monitoring/grafana.yaml +++ b/deploy/k8s/monitoring/grafana.yaml @@ -1,82 +1,82 @@ -apiVersion: v1 -kind: Secret -metadata: - name: grafana-admin - namespace: monitoring -type: Opaque -data: - admin-user: YWRtaW4= - admin-password: Y2hhbmdlLW1l ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: grafana-datasources - namespace: monitoring -data: - datasources.yaml: | - apiVersion: 1 - datasources: - - name: Prometheus - type: prometheus - access: proxy - url: http://prometheus:9090 - isDefault: true - - name: Loki - type: loki - access: proxy - url: http://loki:3100 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: grafana - namespace: monitoring -spec: - replicas: 1 - selector: - matchLabels: - app: grafana - template: - metadata: - labels: - app: grafana - spec: - containers: - - name: grafana - image: grafana/grafana:10.4.6 - ports: - - name: http - containerPort: 3000 - env: - - name: GF_SECURITY_ADMIN_USER - valueFrom: - secretKeyRef: - name: grafana-admin - key: admin-user - - name: GF_SECURITY_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: grafana-admin - key: admin-password - volumeMounts: - - name: datasources - mountPath: /etc/grafana/provisioning/datasources - volumes: - - name: datasources - configMap: - name: grafana-datasources ---- -apiVersion: v1 -kind: Service -metadata: - name: grafana - namespace: monitoring -spec: - type: ClusterIP - ports: - - name: http - port: 3000 - targetPort: http - selector: - app: grafana +apiVersion: v1 +kind: Secret +metadata: + name: grafana-admin + namespace: monitoring +type: Opaque +data: + admin-user: YWRtaW4= + admin-password: Y2hhbmdlLW1l +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-datasources + namespace: monitoring +data: + datasources.yaml: | + apiVersion: 1 + datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + - name: Loki + type: loki + access: proxy + url: http://loki:3100 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: grafana + namespace: monitoring +spec: + replicas: 1 + selector: + matchLabels: + app: grafana + template: + metadata: + labels: + app: grafana + spec: + containers: + - name: grafana + image: grafana/grafana:10.4.6 + ports: + - name: http + containerPort: 3000 + env: + - name: GF_SECURITY_ADMIN_USER + valueFrom: + secretKeyRef: + name: grafana-admin + key: admin-user + - name: GF_SECURITY_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: grafana-admin + key: admin-password + volumeMounts: + - name: datasources + mountPath: /etc/grafana/provisioning/datasources + volumes: + - name: datasources + configMap: + name: grafana-datasources +--- +apiVersion: v1 +kind: Service +metadata: + name: grafana + namespace: monitoring +spec: + type: ClusterIP + ports: + - name: http + port: 3000 + targetPort: http + selector: + app: grafana diff --git a/deploy/k8s/monitoring/loki.yaml b/deploy/k8s/monitoring/loki.yaml index 1fc0897..8e6c209 100644 --- a/deploy/k8s/monitoring/loki.yaml +++ b/deploy/k8s/monitoring/loki.yaml @@ -1,90 +1,90 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: loki-config - namespace: monitoring -data: - loki.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: - kvstore: - store: inmemory - - schema_config: - configs: - - from: 2024-01-01 - store: boltdb-shipper - object_store: filesystem - schema: v12 - index: - prefix: index_ - period: 24h - - storage_config: - boltdb_shipper: - active_index_directory: /loki/index - cache_location: /loki/cache - shared_store: filesystem - - ruler: - alertmanager_url: http://localhost:9093 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: loki - namespace: monitoring -spec: - replicas: 1 - selector: - matchLabels: - app: loki - template: - metadata: - labels: - app: loki - spec: - containers: - - name: loki - image: grafana/loki:2.9.6 - args: - - "-config.file=/etc/loki/loki.yaml" - ports: - - name: http - containerPort: 3100 - volumeMounts: - - name: config - mountPath: /etc/loki - - name: data - mountPath: /loki - volumes: - - name: config - configMap: - name: loki-config - - name: data - emptyDir: {} ---- -apiVersion: v1 -kind: Service -metadata: - name: loki - namespace: monitoring -spec: - type: ClusterIP - ports: - - name: http - port: 3100 - targetPort: http - selector: - app: loki +apiVersion: v1 +kind: ConfigMap +metadata: + name: loki-config + namespace: monitoring +data: + loki.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: + kvstore: + store: inmemory + + schema_config: + configs: + - from: 2024-01-01 + store: boltdb-shipper + object_store: filesystem + schema: v12 + index: + prefix: index_ + period: 24h + + storage_config: + boltdb_shipper: + active_index_directory: /loki/index + cache_location: /loki/cache + shared_store: filesystem + + ruler: + alertmanager_url: http://localhost:9093 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: loki + namespace: monitoring +spec: + replicas: 1 + selector: + matchLabels: + app: loki + template: + metadata: + labels: + app: loki + spec: + containers: + - name: loki + image: grafana/loki:2.9.6 + args: + - "-config.file=/etc/loki/loki.yaml" + ports: + - name: http + containerPort: 3100 + volumeMounts: + - name: config + mountPath: /etc/loki + - name: data + mountPath: /loki + volumes: + - name: config + configMap: + name: loki-config + - name: data + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: loki + namespace: monitoring +spec: + type: ClusterIP + ports: + - name: http + port: 3100 + targetPort: http + selector: + app: loki diff --git a/deploy/k8s/monitoring/prometheus.yaml b/deploy/k8s/monitoring/prometheus.yaml index 54484ff..bb86ef8 100644 --- a/deploy/k8s/monitoring/prometheus.yaml +++ b/deploy/k8s/monitoring/prometheus.yaml @@ -1,138 +1,138 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: prometheus - namespace: monitoring ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: prometheus -rules: - - apiGroups: [""] - resources: - - nodes - - nodes/metrics - - services - - endpoints - - pods - - namespaces - verbs: ["get", "list", "watch"] - - apiGroups: ["extensions", "apps"] - resources: - - deployments - verbs: ["get", "list", "watch"] - - nonResourceURLs: ["/metrics"] - verbs: ["get"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: prometheus -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: prometheus -subjects: - - kind: ServiceAccount - name: prometheus - namespace: monitoring ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: prometheus-config - namespace: monitoring -data: - prometheus.yml: | - global: - scrape_interval: 15s - evaluation_interval: 15s - - scrape_configs: - - job_name: "prometheus" - static_configs: - - targets: ["localhost:9090"] - - - job_name: "kubernetes-annotated-endpoints" - kubernetes_sd_configs: - - role: endpoints - relabel_configs: - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] - action: keep - regex: "true" - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme] - action: replace - target_label: __scheme__ - regex: (https?) - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] - action: replace - target_label: __address__ - regex: (.+):(?:\d+);(\d+) - replacement: $1:$2 - - source_labels: [__meta_kubernetes_namespace] - action: replace - target_label: namespace - - source_labels: [__meta_kubernetes_service_name] - action: replace - target_label: service - - source_labels: [__meta_kubernetes_endpoint_port_name] - action: replace - target_label: port ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: prometheus - namespace: monitoring -spec: - replicas: 1 - selector: - matchLabels: - app: prometheus - template: - metadata: - labels: - app: prometheus - spec: - serviceAccountName: prometheus - containers: - - name: prometheus - image: prom/prometheus:v2.53.0 - args: - - "--config.file=/etc/prometheus/prometheus.yml" - - "--storage.tsdb.path=/prometheus" - - "--storage.tsdb.retention.time=15d" - - "--web.enable-lifecycle" - ports: - - name: http - containerPort: 9090 - volumeMounts: - - name: config - mountPath: /etc/prometheus - - name: data - mountPath: /prometheus - volumes: - - name: config - configMap: - name: prometheus-config - - name: data - emptyDir: {} ---- -apiVersion: v1 -kind: Service -metadata: - name: prometheus - namespace: monitoring -spec: - type: ClusterIP - ports: - - name: http - port: 9090 - targetPort: http - selector: - app: prometheus +apiVersion: v1 +kind: ServiceAccount +metadata: + name: prometheus + namespace: monitoring +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: prometheus +rules: + - apiGroups: [""] + resources: + - nodes + - nodes/metrics + - services + - endpoints + - pods + - namespaces + verbs: ["get", "list", "watch"] + - apiGroups: ["extensions", "apps"] + resources: + - deployments + verbs: ["get", "list", "watch"] + - nonResourceURLs: ["/metrics"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: prometheus +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: prometheus +subjects: + - kind: ServiceAccount + name: prometheus + namespace: monitoring +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-config + namespace: monitoring +data: + prometheus.yml: | + global: + scrape_interval: 15s + evaluation_interval: 15s + + scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + + - job_name: "kubernetes-annotated-endpoints" + kubernetes_sd_configs: + - role: endpoints + relabel_configs: + - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] + action: keep + regex: "true" + - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme] + action: replace + target_label: __scheme__ + regex: (https?) + - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] + action: replace + target_label: __metrics_path__ + regex: (.+) + - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] + action: replace + target_label: __address__ + regex: (.+):(?:\d+);(\d+) + replacement: $1:$2 + - source_labels: [__meta_kubernetes_namespace] + action: replace + target_label: namespace + - source_labels: [__meta_kubernetes_service_name] + action: replace + target_label: service + - source_labels: [__meta_kubernetes_endpoint_port_name] + action: replace + target_label: port +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus + namespace: monitoring +spec: + replicas: 1 + selector: + matchLabels: + app: prometheus + template: + metadata: + labels: + app: prometheus + spec: + serviceAccountName: prometheus + containers: + - name: prometheus + image: prom/prometheus:v2.53.0 + args: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--storage.tsdb.retention.time=15d" + - "--web.enable-lifecycle" + ports: + - name: http + containerPort: 9090 + volumeMounts: + - name: config + mountPath: /etc/prometheus + - name: data + mountPath: /prometheus + volumes: + - name: config + configMap: + name: prometheus-config + - name: data + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: prometheus + namespace: monitoring +spec: + type: ClusterIP + ports: + - name: http + port: 9090 + targetPort: http + selector: + app: prometheus diff --git a/deploy/k8s/monitoring/promtail.yaml b/deploy/k8s/monitoring/promtail.yaml index 985495c..d771301 100644 --- a/deploy/k8s/monitoring/promtail.yaml +++ b/deploy/k8s/monitoring/promtail.yaml @@ -1,149 +1,149 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: promtail - namespace: monitoring ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: promtail -rules: - - apiGroups: [""] - resources: - - nodes - - pods - - pods/log - - services - - endpoints - - namespaces - verbs: ["get", "list", "watch"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: promtail -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: promtail -subjects: - - kind: ServiceAccount - name: promtail - namespace: monitoring ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: promtail-config - namespace: monitoring -data: - promtail.yaml: | - server: - http_listen_port: 9080 - grpc_listen_port: 0 - - positions: - filename: /run/promtail/positions.yaml - - clients: - - url: http://loki:3100/loki/api/v1/push - - scrape_configs: - - job_name: kubernetes-pods - kubernetes_sd_configs: - - role: pod - relabel_configs: - - action: replace - source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name] - target_label: app - regex: (.+) - - action: replace - source_labels: [__meta_kubernetes_pod_label_app] - target_label: app - regex: (.+) - - action: replace - source_labels: [__meta_kubernetes_pod_node_name] - target_label: node - - action: replace - source_labels: [__meta_kubernetes_namespace] - target_label: namespace - - action: replace - source_labels: [__meta_kubernetes_pod_name] - target_label: pod - - action: replace - source_labels: [__meta_kubernetes_pod_container_name] - target_label: container - - action: replace - source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name] - separator: / - target_label: __path__ - replacement: /var/log/pods/*$1/*.log - - job_name: kubernetes-pods-static - pipeline_stages: - - regex: - source: filename - expression: /var/log/pods/(?P[^_]+)_(?P[^_]+)_[^/]+/(?P[^/]+)/[0-9]+\.log - - regex: - source: pod - expression: ^(?P.+?)(?:-[a-f0-9]{8,10}-[a-z0-9]{5}|-[0-9]+)?$ - - labels: - namespace: - pod: - container: - app: - static_configs: - - targets: - - localhost - labels: - job: kubernetes-pods - __path__: /var/log/pods/*/*/*.log ---- -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: promtail - namespace: monitoring -spec: - selector: - matchLabels: - app: promtail - template: - metadata: - labels: - app: promtail - spec: - serviceAccountName: promtail - tolerations: - - operator: "Exists" - containers: - - name: promtail - image: grafana/promtail:2.9.6 - securityContext: - runAsUser: 0 - runAsGroup: 0 - args: - - "-config.file=/etc/promtail/promtail.yaml" - volumeMounts: - - name: config - mountPath: /etc/promtail - - name: positions - mountPath: /run/promtail - - name: varlog - mountPath: /var/log - readOnly: true - - name: dockercontainers - mountPath: /var/lib/docker/containers - readOnly: true - volumes: - - name: config - configMap: - name: promtail-config - - name: positions - emptyDir: {} - - name: varlog - hostPath: - path: /var/log - - name: dockercontainers - hostPath: - path: /var/lib/docker/containers +apiVersion: v1 +kind: ServiceAccount +metadata: + name: promtail + namespace: monitoring +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: promtail +rules: + - apiGroups: [""] + resources: + - nodes + - pods + - pods/log + - services + - endpoints + - namespaces + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: promtail +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: promtail +subjects: + - kind: ServiceAccount + name: promtail + namespace: monitoring +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: promtail-config + namespace: monitoring +data: + promtail.yaml: | + server: + http_listen_port: 9080 + grpc_listen_port: 0 + + positions: + filename: /run/promtail/positions.yaml + + clients: + - url: http://loki:3100/loki/api/v1/push + + scrape_configs: + - job_name: kubernetes-pods + kubernetes_sd_configs: + - role: pod + relabel_configs: + - action: replace + source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name] + target_label: app + regex: (.+) + - action: replace + source_labels: [__meta_kubernetes_pod_label_app] + target_label: app + regex: (.+) + - action: replace + source_labels: [__meta_kubernetes_pod_node_name] + target_label: node + - action: replace + source_labels: [__meta_kubernetes_namespace] + target_label: namespace + - action: replace + source_labels: [__meta_kubernetes_pod_name] + target_label: pod + - action: replace + source_labels: [__meta_kubernetes_pod_container_name] + target_label: container + - action: replace + source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name] + separator: / + target_label: __path__ + replacement: /var/log/pods/*$1/*.log + - job_name: kubernetes-pods-static + pipeline_stages: + - regex: + source: filename + expression: /var/log/pods/(?P[^_]+)_(?P[^_]+)_[^/]+/(?P[^/]+)/[0-9]+\.log + - regex: + source: pod + expression: ^(?P.+?)(?:-[a-f0-9]{8,10}-[a-z0-9]{5}|-[0-9]+)?$ + - labels: + namespace: + pod: + container: + app: + static_configs: + - targets: + - localhost + labels: + job: kubernetes-pods + __path__: /var/log/pods/*/*/*.log +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: promtail + namespace: monitoring +spec: + selector: + matchLabels: + app: promtail + template: + metadata: + labels: + app: promtail + spec: + serviceAccountName: promtail + tolerations: + - operator: "Exists" + containers: + - name: promtail + image: grafana/promtail:2.9.6 + securityContext: + runAsUser: 0 + runAsGroup: 0 + args: + - "-config.file=/etc/promtail/promtail.yaml" + volumeMounts: + - name: config + mountPath: /etc/promtail + - name: positions + mountPath: /run/promtail + - name: varlog + mountPath: /var/log + readOnly: true + - name: dockercontainers + mountPath: /var/lib/docker/containers + readOnly: true + volumes: + - name: config + configMap: + name: promtail-config + - name: positions + emptyDir: {} + - name: varlog + hostPath: + path: /var/log + - name: dockercontainers + hostPath: + path: /var/lib/docker/containers diff --git a/deploy/k8s/secrets/jwt-secret.yaml b/deploy/k8s/secrets/jwt-secret.yaml index 7d7e643..b630543 100644 --- a/deploy/k8s/secrets/jwt-secret.yaml +++ b/deploy/k8s/secrets/jwt-secret.yaml @@ -1,67 +1,67 @@ -apiVersion: v1 -kind: Secret -metadata: - name: jwt-secret - namespace: juwan -type: Opaque -data: - secret-key: MGUyMWE3ZDhjMTQ5ZDg1MWViOWU0MGM3OTE2NWVkYTBlOTE5ZWRkZDU1YjYzOGJjOWRiNzM0NTc4NDIyMjlkZQ== ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: user-rpc - namespace: juwan ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: envoy-gateway - namespace: juwan ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: jwt-secret-reader - namespace: juwan -rules: - # JWT Secret 读取权限 - - apiGroups: [""] - resources: ["secrets"] - resourceNames: ["jwt-secret"] - verbs: ["get"] - # 服务发现权限 - - apiGroups: [""] - resources: ["endpoints"] - verbs: ["get", "list", "watch"] - - apiGroups: ["discovery.k8s.io"] - resources: ["endpointslices"] - verbs: ["get", "list", "watch"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: user-rpc-jwt-secret-reader - namespace: juwan -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: jwt-secret-reader -subjects: - - kind: ServiceAccount - name: user-rpc - namespace: juwan ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: envoy-gateway-jwt-secret-reader - namespace: juwan -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: jwt-secret-reader -subjects: - - kind: ServiceAccount - name: envoy-gateway - namespace: juwan +apiVersion: v1 +kind: Secret +metadata: + name: jwt-secret + namespace: juwan +type: Opaque +data: + secret-key: MGUyMWE3ZDhjMTQ5ZDg1MWViOWU0MGM3OTE2NWVkYTBlOTE5ZWRkZDU1YjYzOGJjOWRiNzM0NTc4NDIyMjlkZQ== +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: user-rpc + namespace: juwan +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: envoy-gateway + namespace: juwan +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: jwt-secret-reader + namespace: juwan +rules: + # JWT Secret 读取权限 + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["jwt-secret"] + verbs: ["get"] + # 服务发现权限 + - apiGroups: [""] + resources: ["endpoints"] + verbs: ["get", "list", "watch"] + - apiGroups: ["discovery.k8s.io"] + resources: ["endpointslices"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: user-rpc-jwt-secret-reader + namespace: juwan +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: jwt-secret-reader +subjects: + - kind: ServiceAccount + name: user-rpc + namespace: juwan +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: envoy-gateway-jwt-secret-reader + namespace: juwan +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: jwt-secret-reader +subjects: + - kind: ServiceAccount + name: envoy-gateway + namespace: juwan diff --git a/docs/PROJECT_GUIDE.md b/docs/PROJECT_GUIDE.md index c65ebcf..923da29 100644 --- a/docs/PROJECT_GUIDE.md +++ b/docs/PROJECT_GUIDE.md @@ -1,1032 +1,1032 @@ -# Juwan 后端项目完整使用指南 - -## 项目概述 - -``` -Juwan 是一个基于 Go-Zero 微服务框架的分布式后端系统,采用以下架构: - -┌─────────────────────────────────────────────────────────────────────┐ -│ Envoy Gateway (负载均衡、认证) │ -│ 端口: 80 (HTTP) │ -└──────────────┬──────────────────────────────────────────────────────┘ - │ - ┌───────┴──────────┐ - │ │ - ┌───▼────────┐ ┌───▼────────┐ - │ User API │ │ Order API │ - │ (8888) │ │ (8889) │ - └───┬────────┘ └────────────┘ - │ - ┌───▼────────────────────┐ - │ User RPC (内部使用) │ - │ gRPC (不暴露) │ - └────────────────────────┘ - │ - ┌───▼────────────────────┐ - │ PostgreSQL Database │ - └────────────────────────┘ -``` - -**关键特性:** -- ✅ API 层通过 Envoy Gateway 暴露给外部 -- ✅ RPC 层仅限集群内部通信(通过 K8s Service Discovery) -- ✅ JWT 认证(可选路由) -- ✅ 密码加密存储 -- ✅ 用户会话管理 - ---- - -## 1️⃣ 添加新服务(完整步骤) - -### 示例:添加一个 Product API 服务 - -#### Step 1: 定义 API 接口 - -创建 `desc/api/product.api`: - -```goctl -syntax = "v1" - -type ( - Product { - ProductId int64 `json:"productId"` - Name string `json:"name"` - Description string `json:"description"` - Price float64 `json:"price"` - Stock int32 `json:"stock"` - CreateAt int64 `json:"createAt"` - } - - CreateProductReq { - Name string `json:"name" binding:"required,min=2"` - Description string `json:"description"` - Price float64 `json:"price" binding:"required,gt=0"` - Stock int32 `json:"stock" binding:"required,gte=0"` - } - - CreateProductResp { - ProductId int64 `json:"productId"` - Message string `json:"message"` - } - - GetProductReq { - ProductId int64 `path:"productId" binding:"required,gt=0"` - } - - ListProductsReq { - Page int64 `form:"page" binding:"required,gt=0"` - Limit int64 `form:"limit" binding:"required,gt=0,lte=100"` - } - - ListProductsResp { - Total int64 `json:"total"` - Products []Product `json:"products"` - } -) - -@server ( - group: product - prefix: /api/products - middleware: Logger -) -service product-api { - @doc (summary: "创建商品") - @handler CreateProduct - post / (CreateProductReq) returns (CreateProductResp) - - @doc (summary: "获取商品详情") - @handler GetProduct - get /:productId (GetProductReq) returns (Product) - - @doc (summary: "列出所有商品") - @handler ListProducts - get / (ListProductsReq) returns (ListProductsResp) -} -``` - -#### Step 2: 创建 RPC 定义(内部服务) - -创建 `desc/rpc/product.proto`: - -```proto -syntax = "proto3"; - -option go_package = "./pb"; - -package pb; - -message Product { - int64 productId = 1; - string name = 2; - string description = 3; - double price = 4; - int32 stock = 5; - int64 createAt = 6; -} - -message GetProductReq { - int64 productId = 1; -} - -message GetProductResp { - Product product = 1; -} - -message UpdateStockReq { - int64 productId = 1; - int32 delta = 2; // 正数增加库存,负数减少 -} - -message UpdateStockResp { - int32 newStock = 1; -} - -service ProductCenter { - rpc GetProduct(GetProductReq) returns (GetProductResp); - rpc UpdateStock(UpdateStockReq) returns (UpdateStockResp); -} -``` - -#### Step 3: 生成代码 - -```bash -# 生成 API 服务代码 -goctl api go -api desc/api/product.api -dir app/product/api --style=goZero - -# 生成 RPC 服务代码 -goctl rpc protoc desc/rpc/product.proto \ - --go_out=./app/product/rpc \ - --go-grpc_out=./app/product/rpc \ - --zrpc_out=./app/product/rpc \ - --style=goZero -``` - -#### Step 4: 实现业务逻辑 - -编辑 `app/product/api/internal/logic/product/createproduct.go`: - -```go -package product - -import ( - "context" - "product-api/app/product/api/internal/svc" - "product-api/app/product/api/internal/types" -) - -type CreateProductLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewCreateProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateProductLogic { - return &CreateProductLogic{ - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CreateProductLogic) CreateProduct(req *types.CreateProductReq) (*types.CreateProductResp, error) { - // TODO: 调用 RPC 或数据库创建商品 - return &types.CreateProductResp{ - ProductId: 1, - Message: "创建成功", - }, nil -} -``` - -#### Step 5: 配置 K8s 部署 - -创建 `deploy/k8s/service/product/product-api.yaml`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: product-api-config - namespace: juwan -data: - product-api.yaml: | - Name: product-api - Host: 0.0.0.0 - Port: 8890 - Database: - DataSource: postgres://user:pass@pg-dx:5432/juwan - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: product-api - namespace: juwan -spec: - replicas: 2 - selector: - matchLabels: - app: product-api - template: - metadata: - labels: - app: product-api - spec: - containers: - - name: product-api - image: your-registry/product-api:latest - ports: - - containerPort: 8890 - volumeMounts: - - name: config - mountPath: /etc/product-api - env: - - name: TZ - value: "Asia/Shanghai" - volumes: - - name: config - configMap: - name: product-api-config - ---- -apiVersion: v1 -kind: Service -metadata: - name: product-api-svc - namespace: juwan -spec: - selector: - app: product-api - ports: - - port: 8890 - targetPort: 8890 - type: ClusterIP -``` - -#### Step 6: 更新 Envoy 网关配置 - -在 `deploy/k8s/envoy-gateway.yaml` 中添加: - -```yaml -# 在 http_filters->route_config->virtual_hosts->routes 添加: -- match: - prefix: /api/products - route: - cluster: product_api_cluster - timeout: 30s - -# 在 clusters 添加: -- name: product_api_cluster - connect_timeout: 5s - type: STRICT_DNS - dns_lookup_family: V4_ONLY - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: product_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: product-api-svc.juwan.svc.cluster.local - port_value: 8890 - health_checks: - - timeout: 3s - interval: 10s - unhealthy_threshold: 2 - healthy_threshold: 2 - http_health_check: - path: /health -``` - -#### Step 7: 部署到集群 - -```bash -# 应用 K8s 配置 -kubectl apply -f deploy/k8s/service/product/product-api.yaml - -# 更新 Envoy Gateway 配置 -kubectl apply -f deploy/k8s/envoy-gateway.yaml - -# 重启 Envoy Pod 使配置生效 -kubectl delete pods -n juwan -l app=envoy-gateway - -# 验证 -kubectl get pods -n juwan -curl http://localhost/api/products -``` - ---- - -## 2️⃣ RPC 服务内部隔离(不暴露给外部) - -### 当前架构(推荐) - -``` -┌────────────────────────────────────────────┐ -│ 外部客户端 (互联网) │ -└────────────┬─────────────────────────────┘ - │ - ┌────▼──────────┐ - │ Envoy Gateway│ - │ (仅 HTTP) │ - └────┬──────────┘ - │ - ┌────────┴─────────────┐ - │ │ -┌───▼───────────┐ ┌──────▼─────────┐ -│ User API │ │ Product API │ -│ (8888) │ │ (8890) │ -└───┬───────────┘ └────────────────┘ - │ -┌───▼──────────────────────────────────┐ -│ User RPC (完全隐藏) │ -│ - 不暴露端口 │ -│ - gRPC 通信 │ -│ - K8s Service DNS 发现 │ -│ - NetworkPolicy 限制通信 │ -└──────────────────────────────────────┘ -``` - -### 实现步骤 - -#### 1. 创建仅内部的 Service(无 port 暴露) - -在 `deploy/k8s/service/user/user-rpc.yaml`: - -```yaml -apiVersion: v1 -kind: Service -metadata: - name: user-rpc-svc - namespace: juwan -spec: - selector: - app: user-rpc - ports: - - name: grpc - port: 50051 - targetPort: 50051 - type: ClusterIP # 📌 仅限集群内部访问,不暴露 NodePort 或 LoadBalancer - sessionAffinity: None -``` - -#### 2. 配置 NetworkPolicy(进一步限制) - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: user-rpc-access - namespace: juwan -spec: - podSelector: - matchLabels: - app: user-rpc - policyTypes: - - Ingress - ingress: - # 仅允许 API 和其他服务(如 Order API)访问 - - from: - - podSelector: - matchLabels: - app: user-api - - podSelector: - matchLabels: - app: order-api - - podSelector: - matchLabels: - app: product-api - ports: - - protocol: TCP - port: 50051 -``` - -#### 3. API 服务中调用 RPC - -在 `app/users/api/internal/svc/servicecontext.go`: - -```go -package svc - -import ( - "github.com/zeromicro/go-zero/zrpc" - "app/users/api/internal/config" - "app/users/rpc/pb" -) - -type ServiceContext struct { - Config config.Config - UserRpc pb.UsercenterClient // RPC 客户端 -} - -func NewServiceContext(c config.Config) *ServiceContext { - userRpcClient := zrpc.MustNewClient(c.UserRpc) - return &ServiceContext{ - Config: c, - UserRpc: pb.NewUsercenterClient(userRpcClient.Conn()), - } -} -``` - -配置文件 `app/users/api/etc/user-api.yaml`: - -```yaml -Name: user-api -Host: 0.0.0.0 -Port: 8888 - -# RPC 配置(使用 K8s DNS) -UserRpc: - Endpoints: - - user-rpc-svc.juwan.svc.cluster.local:50051 - -Database: - DataSource: postgres://user:pass@pg-dx:5432/juwan -``` - -#### 4. RPC 不在 Envoy 中配置路由 - -❌ **不要**在 `envoy-gateway.yaml` 中添加 RPC 集群: - -```yaml -# ❌❌❌ 不要这样做 ❌❌❌ -# routes: -# - match: -# prefix: /juwan.pb.Usercenter/ -# route: -# cluster: user_rpc_cluster # 这样会暴露 RPC -``` - ---- - -## 3️⃣ JWT 认证与分级访问控制 - -### 实现逻辑 - -``` -请求到达 Envoy → JWT 验证 - -┌──────────────────────────────────────┐ -│ 无效或缺省 Token │ -└──────────┬───────────────────────────┘ - │ - 公开路由 (允许) ──→ /api/users/login - /api/users/register - /api/products (列表、详情) - - 受保护路由 (拒绝) ──→ 返回 401 Unauthorized - │ - ┌──────▼───────────────────────────┐ - │ 有效 Token │ - │ (已登录) │ - └──────┬───────────────────────────┘ - │ - ┌──────▼──────────────────────────────────────────────┐ - │ 完整数据访问 (取决于后端 RPC) │ - │ - 用户信息 (包括隐私信息) │ - │ - 订单历史 │ - │ - 收藏列表 │ - └──────────────────────────────────────────────────────┘ -``` - -### 配置步骤 - -#### 1. 生成 JWT 密钥并存储为 K8s Secret - -```bash -# 生成 HMAC 密钥(用于签名) -openssl rand -hex 32 - -# 输出示例: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 - -# 创建 Secret -kubectl create secret generic jwt-secret \ - --from-literal=key=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \ - -n juwan - -# 验证 -kubectl get secret jwt-secret -n juwan -o yaml -``` - -#### 2. 更新 Envoy 配置(添加 JWT 验证) - -编辑 `deploy/k8s/envoy-gateway.yaml`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: envoy-config - namespace: juwan -data: - envoy.yaml: | - static_resources: - listeners: - - name: listener_http - address: - socket_address: - address: 0.0.0.0 - port_value: 8080 - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: ingress_http - http_filters: - # JWT 认证过滤器(在 router 之前) - - name: envoy.filters.http.jwt_authn - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication - - # 定义 JWT 提供者 - providers: - my-provider: - issuer: "juwan" - audiences: "api" - # 使用文件系统上的密钥 - local_jwks: - filename: /etc/envoy/jwks.json - - # 定义受保护的路由 - rules: - # 规则1: 登录和注册不需要认证 - - match: - prefix: /api/users/login - allow_missing_or_failed: true # 允许缺省/失败的 token - - match: - prefix: /api/users/register - allow_missing_or_failed: true - # 规则2: 重定向认证失败请求 - - match: - prefix: "/" - requires: - provider_name: "my-provider" - # 如果认证失败,Envoy 直接拒绝,返回 401 - - # 路由过滤器 - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - - route_config: - name: local_route - virtual_hosts: - - name: backend - domains: ["*"] - routes: - - match: - prefix: /api/users - route: - cluster: user_api_cluster - - match: - prefix: /api/products - route: - cluster: product_api_cluster - # ... 其他路由 ... - - # ... clusters 定义保持不变 ... -``` - -#### 3. 在 API 服务中添加认证中间件 - -创建 `app/users/api/internal/middleware/authmiddleware.go`: - -```go -package middleware - -import ( - "fmt" - "net/http" - "strings" - - "github.com/golang-jwt/jwt/v4" -) - -type AuthMiddleware struct { - JwtSecret string -} - -func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // 获取 Authorization header - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - // 如果认证失败,Envoy 会返回 401, - // 但我们可以在 API 层添加自定义逻辑 - next(w, r) - return - } - - parts := strings.Split(authHeader, " ") - if len(parts) != 2 || parts[0] != "Bearer" { - http.Error(w, "Invalid authorization header", http.StatusUnauthorized) - return - } - - token := parts[1] - - // 验证 token - _, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(m.JwtSecret), nil - }) - - if err != nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - // Token 有效,继续处理 - next(w, r) - } -} -``` - -#### 4. 登录端点返回 JWT Token - -编辑 `app/users/api/internal/logic/user/loginlogic.go`: - -```go -package user - -import ( - "context" - "time" - "github.com/golang-jwt/jwt/v4" - "app/users/api/internal/svc" - "app/users/api/internal/types" -) - -type LoginLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext -} - -func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginResp, error) { - // TODO: 验证用户名和密码 - - // 生成 JWT Token - claims := jwt.MapClaims{ - "userId": 1, // 实际应从数据库获取 - "username": req.Username, - "exp": time.Now().Add(24 * time.Hour).Unix(), - "iat": time.Now().Unix(), - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(l.svcCtx.Config.JwtSecret)) - if err != nil { - return nil, err - } - - return &types.LoginResp{ - Token: tokenString, - Expires: time.Now().Add(24 * time.Hour).Unix(), - }, nil -} -``` - -### JWT 认证时的分级访问 - -**后端 RPC 可处理分级访问:** - -```go -// 在 User RPC 中实现 -func (s *UsercenterServer) GetUserInfo(ctx context.Context, req *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { - // 获取请求者的 userId(从 context 中取,由 API 层传递) - requesterID := ctx.Value("userId").(int64) - targetID := req.Id - - // 查询用户信息 - user := s.getUserFromDB(targetID) - - if requesterID == targetID { - // 自己查看自己 → 返回完整信息(包含隐私信息) - return &pb.GetUsersByIdResp{ - Users: &pb.Users{ - UserId: user.UserId, - Username: user.Username, - Email: user.Email, // ✅ 包含 - Phone: user.Phone, // ✅ 包含 - // ... 所有字段 - }, - }, nil - } else { - // 查看别人 → 返回部分信息 - return &pb.GetUsersByIdResp{ - Users: &pb.Users{ - UserId: user.UserId, - Username: user.Username, - // ❌ 不返回 Email、Phone 等隐私信息 - }, - }, nil - } -} -``` - -**或在 API 层处理:** - -```go -func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (*types.UserInfo, error) { - // 从 context 获取当前认证用户 - currentUser := l.ctx.Value("userId").(int64) - - // 调用 RPC - rpcResp, err := l.svcCtx.UserRpc.GetUsersById(l.ctx, &pb.GetUsersByIdReq{ - Id: req.UserId, - }) - - if currentUser == req.UserId { - // 自己查看自己 → 返回所有信息 - return &types.UserInfo{ - UserId: rpcResp.Users.UserId, - Username: rpcResp.Users.Username, - Email: rpcResp.Users.Email, - Phone: rpcResp.Users.Phone, - }, nil - } else { - // 查看别人 → 仅返回公开信息 - return &types.UserInfo{ - UserId: rpcResp.Users.UserId, - Username: rpcResp.Users.Username, - }, nil - } -} -``` - ---- - -## 4️⃣ 认证失败处理策略 - -### 当前配置 - -| 路由 | 认证要求 | 认证失败 | 示例 | -|-----|---------|--------|------| -| `/api/users/login` | ❌ 不需要 | 放行 | `POST /api/users/login` (允许) | -| `/api/users/register` | ❌ 不需要 | 放行 | `POST /api/users/register` (允许) | -| `/api/users/:userId` | ✅ 需要 | 拒绝 401 | `GET /api/users/123` (无 token → 401) | -| `/api/users/:userId/password` | ✅ 需要 | 拒绝 401 | `PUT /api/users/123/password` (无 token → 401) | -| `/api/products` | ❌ 不需要 | 放行 | `GET /api/products` (允许) | -| `/api/products/:id` | ❌ 不需要 | 放行 | `GET /api/products/1` (允许) | - -### 自定义错误响应 - -创建 `deploy/k8s/envoy-gateway.yaml` 的自定义拒绝响应: - -```yaml -# 在 jwt_authn 过滤器中配置 -http_filters: - - name: envoy.filters.http.jwt_authn - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication - providers: - my-provider: - issuer: "juwan" - audiences: "api" - local_jwks: - filename: /etc/envoy/jwks.json - rules: - - match: - prefix: /api/users/login - allow_missing_or_failed: true - - match: - prefix: /api/users/register - allow_missing_or_failed: true - - match: - prefix: / - requires: - provider_name: "my-provider" - # 认证失败后的行为 - bypass_cors_preflight: false # 允许 OPTIONS 跨域请求 -``` - -**认证失败时 Envoy 返回 401:** - -```json -HTTP/1.1 401 Unauthorized -content-type: text/html - -Jwt verification fails. -``` - -### 在 API 层添加自定义错误处理 - -如果希望自定义错误响应,可以在每个受保护的 handler 中添加检查: - -```go -func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (*types.UpdateUserInfoResp, error) { - // 获取当前请求者的身份 - ctx := context.WithValue(l.ctx, "currentUserId", req.UserId) - - // 如果没有匹配的 token,API 层可以添加自定义错误 - if req.UserId <= 0 { - return nil, errors.New("invalid user id") - } - - // 继续处理 - return &types.UpdateUserInfoResp{Message: "更新成功"}, nil -} -``` - ---- - -## 5️⃣ 完整工作流示例 - -### 场景:用户注册 → 登录 → 获取用户信息 - -#### 步骤 1: 注册用户(无需认证) - -```bash -curl -X POST http://localhost/api/users/register \ - -H "Content-Type: application/json" \ - -d '{ - "username": "john_doe", - "password": "securePass123", - "email": "john@example.com", - "phone": "13800138001" - }' - -# 响应 -{ - "userId": 1, - "username": "john_doe", - "email": "john@example.com", - "message": "用户注册成功" -} -``` - -#### 步骤 2: 登录获取 Token(无需认证) - -```bash -curl -X POST http://localhost/api/users/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "john_doe", - "password": "securePass123" - }' - -# 响应 -{ - "userId": 1, - "username": "john_doe", - "email": "john@example.com", - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "expires": 1708694400 -} -``` - -#### 步骤 3: 获取用户信息(需要 Token) - -```bash -# ❌ 无 Token → 401 Unauthorized -curl http://localhost/api/users/1 -# Jwt verification fails. - -# ✅ 有 Token → 成功 -curl http://localhost/api/users/1 \ - -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - -# 响应 -{ - "userId": 1, - "username": "john_doe", - "email": "john@example.com", - "phone": "13800138001", - "avatar": "https://...", - "status": 1, - "createAt": 1708608000, - "updateAt": 1708608000 -} -``` - -#### 步骤 4: 查看他人信息(部分数据) - -```bash -# 用户1 查看用户2 的信息(已登录) -curl http://localhost/api/users/2 \ - -H "Authorization: Bearer eyJhbGc..." - -# 响应(仅公开信息) -{ - "userId": 2, - "username": "jane_doe", - # ❌ 不包含 email, phone 等隐私信息 -} -``` - ---- - -## 6️⃣ 部署检查清单 - -在部署新服务或更新配置前,使用此清单: - -- [ ] **API 定义** - `desc/api/*.api` 文件已创建 -- [ ] **RPC 定义** - `desc/rpc/*.proto` 文件已创建(如需内部通信) -- [ ] **代码生成** - 运行 `goctl api/rpc` 命令生成代码 -- [ ] **业务逻辑** - 编辑 `app/*/api/internal/logic/` 实现功能 -- [ ] **K8s 部署清单** - 创建 `deploy/k8s/service/*/` 文件 - - [ ] ConfigMap(配置) - - [ ] Deployment(部署) - - [ ] Service(K8s 服务发现) - - [ ] NetworkPolicy(网络隔离,可选) -- [ ] **Envoy 更新** - 修改 `deploy/k8s/envoy-gateway.yaml` - - [ ] 添加路由规则 - - [ ] 添加上游集群 - - [ ] 验证健康检查地址 -- [ ] **测试验证** - ```bash - kubectl apply -f deploy/k8s/service/{name}/ - kubectl apply -f deploy/k8s/envoy-gateway.yaml - kubectl delete pods -n juwan -l app=envoy-gateway - curl http://localhost/api/{path} # 端口转发后测试 - ``` - ---- - -## 7️⃣ 故障排查 - -### 问题 1: 新服务无法访问 - -```bash -# 检查 Pod 状态 -kubectl get pods -n juwan - -# 查看 Pod 日志 -kubectl logs -n juwan -l app=your-service --tail=100 - -# 检查 Service -kubectl get svc -n juwan - -# 测试 DNS 解析 -kubectl exec -it {pod-name} -n juwan -- nslookup your-service-svc.juwan.svc.cluster.local -``` - -### 问题 2: Envoy 配置错误 - -```bash -# 查看 Envoy Pod 日志 -kubectl logs -n juwan -l app=envoy-gateway --tail=50 - -# 常见错误 -# - "no such field" → YAML 字段名与 Envoy 版本不兼容 -# - "Unknown cluster" → Envoy 配置中缺少 cluster 定义 -# - "Connection refused" → 后端服务未启动或 DNS 无法解析 -``` - -### 问题 3: JWT 认证失败 - -```bash -# 检查 JWT 配置是否正确 -kubectl get configmap envoy-config -n juwan -o yaml - -# 查看 jwks.json 是否存在 -kubectl exec -it {envoy-pod} -n juwan -- cat /etc/envoy/jwks.json - -# 验证 Token 格式 -curl -H "Authorization: Bearer {token}" http://localhost/api/users/1 -``` - ---- - -## 文件组织总结 - -``` -project-root/ -├── desc/ # 接口定义 -│ ├── api/ -│ │ ├── users.api # User API 定义 -│ │ └── product.api # ← 新增:Product API 定义 -│ ├── rpc/ -│ │ ├── users.proto # User RPC 定义 -│ │ └── product.proto # ← 新增:Product RPC 定义 -│ └── sql/ -│ -├── app/ # 应用代码 -│ ├── users/ -│ │ ├── api/ # User API 实现 -│ │ └── rpc/ # User RPC 实现(内部) -│ └── product/ # ← 新增:Product 服务 -│ ├── api/ -│ └── rpc/ -│ -├── deploy/ # K8s 部署 -│ ├── k8s/ -│ │ ├── envoy-gateway.yaml # ← 更新:添加新路由 -│ │ ├── base/ -│ │ └── service/ -│ │ ├── user/ -│ │ │ ├── user-api.yaml -│ │ │ └── user-rpc.yaml -│ │ └── product/ # ← 新增:Product 部署文件 -│ │ ├── product-api.yaml -│ │ └── product-rpc.yaml -│ └── envoy/ # 参考配置 -│ -└── docs/ # 文档 - └── PROJECT_GUIDE.md # ← 本文件 -``` - ---- - -希望这份指南能帮助你快速上手项目!有任何问题欢迎提出 📝 +# Juwan 后端项目完整使用指南 + +## 项目概述 + +``` +Juwan 是一个基于 Go-Zero 微服务框架的分布式后端系统,采用以下架构: + +┌─────────────────────────────────────────────────────────────────────┐ +│ Envoy Gateway (负载均衡、认证) │ +│ 端口: 80 (HTTP) │ +└──────────────┬──────────────────────────────────────────────────────┘ + │ + ┌───────┴──────────┐ + │ │ + ┌───▼────────┐ ┌───▼────────┐ + │ User API │ │ Order API │ + │ (8888) │ │ (8889) │ + └───┬────────┘ └────────────┘ + │ + ┌───▼────────────────────┐ + │ User RPC (内部使用) │ + │ gRPC (不暴露) │ + └────────────────────────┘ + │ + ┌───▼────────────────────┐ + │ PostgreSQL Database │ + └────────────────────────┘ +``` + +**关键特性:** +- ✅ API 层通过 Envoy Gateway 暴露给外部 +- ✅ RPC 层仅限集群内部通信(通过 K8s Service Discovery) +- ✅ JWT 认证(可选路由) +- ✅ 密码加密存储 +- ✅ 用户会话管理 + +--- + +## 1️⃣ 添加新服务(完整步骤) + +### 示例:添加一个 Product API 服务 + +#### Step 1: 定义 API 接口 + +创建 `desc/api/product.api`: + +```goctl +syntax = "v1" + +type ( + Product { + ProductId int64 `json:"productId"` + Name string `json:"name"` + Description string `json:"description"` + Price float64 `json:"price"` + Stock int32 `json:"stock"` + CreateAt int64 `json:"createAt"` + } + + CreateProductReq { + Name string `json:"name" binding:"required,min=2"` + Description string `json:"description"` + Price float64 `json:"price" binding:"required,gt=0"` + Stock int32 `json:"stock" binding:"required,gte=0"` + } + + CreateProductResp { + ProductId int64 `json:"productId"` + Message string `json:"message"` + } + + GetProductReq { + ProductId int64 `path:"productId" binding:"required,gt=0"` + } + + ListProductsReq { + Page int64 `form:"page" binding:"required,gt=0"` + Limit int64 `form:"limit" binding:"required,gt=0,lte=100"` + } + + ListProductsResp { + Total int64 `json:"total"` + Products []Product `json:"products"` + } +) + +@server ( + group: product + prefix: /api/products + middleware: Logger +) +service product-api { + @doc (summary: "创建商品") + @handler CreateProduct + post / (CreateProductReq) returns (CreateProductResp) + + @doc (summary: "获取商品详情") + @handler GetProduct + get /:productId (GetProductReq) returns (Product) + + @doc (summary: "列出所有商品") + @handler ListProducts + get / (ListProductsReq) returns (ListProductsResp) +} +``` + +#### Step 2: 创建 RPC 定义(内部服务) + +创建 `desc/rpc/product.proto`: + +```proto +syntax = "proto3"; + +option go_package = "./pb"; + +package pb; + +message Product { + int64 productId = 1; + string name = 2; + string description = 3; + double price = 4; + int32 stock = 5; + int64 createAt = 6; +} + +message GetProductReq { + int64 productId = 1; +} + +message GetProductResp { + Product product = 1; +} + +message UpdateStockReq { + int64 productId = 1; + int32 delta = 2; // 正数增加库存,负数减少 +} + +message UpdateStockResp { + int32 newStock = 1; +} + +service ProductCenter { + rpc GetProduct(GetProductReq) returns (GetProductResp); + rpc UpdateStock(UpdateStockReq) returns (UpdateStockResp); +} +``` + +#### Step 3: 生成代码 + +```bash +# 生成 API 服务代码 +goctl api go -api desc/api/product.api -dir app/product/api --style=goZero + +# 生成 RPC 服务代码 +goctl rpc protoc desc/rpc/product.proto \ + --go_out=./app/product/rpc \ + --go-grpc_out=./app/product/rpc \ + --zrpc_out=./app/product/rpc \ + --style=goZero +``` + +#### Step 4: 实现业务逻辑 + +编辑 `app/product/api/internal/logic/product/createproduct.go`: + +```go +package product + +import ( + "context" + "product-api/app/product/api/internal/svc" + "product-api/app/product/api/internal/types" +) + +type CreateProductLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCreateProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateProductLogic { + return &CreateProductLogic{ + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateProductLogic) CreateProduct(req *types.CreateProductReq) (*types.CreateProductResp, error) { + // TODO: 调用 RPC 或数据库创建商品 + return &types.CreateProductResp{ + ProductId: 1, + Message: "创建成功", + }, nil +} +``` + +#### Step 5: 配置 K8s 部署 + +创建 `deploy/k8s/service/product/product-api.yaml`: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: product-api-config + namespace: juwan +data: + product-api.yaml: | + Name: product-api + Host: 0.0.0.0 + Port: 8890 + Database: + DataSource: postgres://user:pass@pg-dx:5432/juwan + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: product-api + namespace: juwan +spec: + replicas: 2 + selector: + matchLabels: + app: product-api + template: + metadata: + labels: + app: product-api + spec: + containers: + - name: product-api + image: your-registry/product-api:latest + ports: + - containerPort: 8890 + volumeMounts: + - name: config + mountPath: /etc/product-api + env: + - name: TZ + value: "Asia/Shanghai" + volumes: + - name: config + configMap: + name: product-api-config + +--- +apiVersion: v1 +kind: Service +metadata: + name: product-api-svc + namespace: juwan +spec: + selector: + app: product-api + ports: + - port: 8890 + targetPort: 8890 + type: ClusterIP +``` + +#### Step 6: 更新 Envoy 网关配置 + +在 `deploy/k8s/envoy-gateway.yaml` 中添加: + +```yaml +# 在 http_filters->route_config->virtual_hosts->routes 添加: +- match: + prefix: /api/products + route: + cluster: product_api_cluster + timeout: 30s + +# 在 clusters 添加: +- name: product_api_cluster + connect_timeout: 5s + type: STRICT_DNS + dns_lookup_family: V4_ONLY + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: product_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: product-api-svc.juwan.svc.cluster.local + port_value: 8890 + health_checks: + - timeout: 3s + interval: 10s + unhealthy_threshold: 2 + healthy_threshold: 2 + http_health_check: + path: /health +``` + +#### Step 7: 部署到集群 + +```bash +# 应用 K8s 配置 +kubectl apply -f deploy/k8s/service/product/product-api.yaml + +# 更新 Envoy Gateway 配置 +kubectl apply -f deploy/k8s/envoy-gateway.yaml + +# 重启 Envoy Pod 使配置生效 +kubectl delete pods -n juwan -l app=envoy-gateway + +# 验证 +kubectl get pods -n juwan +curl http://localhost/api/products +``` + +--- + +## 2️⃣ RPC 服务内部隔离(不暴露给外部) + +### 当前架构(推荐) + +``` +┌────────────────────────────────────────────┐ +│ 外部客户端 (互联网) │ +└────────────┬─────────────────────────────┘ + │ + ┌────▼──────────┐ + │ Envoy Gateway│ + │ (仅 HTTP) │ + └────┬──────────┘ + │ + ┌────────┴─────────────┐ + │ │ +┌───▼───────────┐ ┌──────▼─────────┐ +│ User API │ │ Product API │ +│ (8888) │ │ (8890) │ +└───┬───────────┘ └────────────────┘ + │ +┌───▼──────────────────────────────────┐ +│ User RPC (完全隐藏) │ +│ - 不暴露端口 │ +│ - gRPC 通信 │ +│ - K8s Service DNS 发现 │ +│ - NetworkPolicy 限制通信 │ +└──────────────────────────────────────┘ +``` + +### 实现步骤 + +#### 1. 创建仅内部的 Service(无 port 暴露) + +在 `deploy/k8s/service/user/user-rpc.yaml`: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: user-rpc-svc + namespace: juwan +spec: + selector: + app: user-rpc + ports: + - name: grpc + port: 50051 + targetPort: 50051 + type: ClusterIP # 📌 仅限集群内部访问,不暴露 NodePort 或 LoadBalancer + sessionAffinity: None +``` + +#### 2. 配置 NetworkPolicy(进一步限制) + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: user-rpc-access + namespace: juwan +spec: + podSelector: + matchLabels: + app: user-rpc + policyTypes: + - Ingress + ingress: + # 仅允许 API 和其他服务(如 Order API)访问 + - from: + - podSelector: + matchLabels: + app: user-api + - podSelector: + matchLabels: + app: order-api + - podSelector: + matchLabels: + app: product-api + ports: + - protocol: TCP + port: 50051 +``` + +#### 3. API 服务中调用 RPC + +在 `app/users/api/internal/svc/servicecontext.go`: + +```go +package svc + +import ( + "github.com/zeromicro/go-zero/zrpc" + "app/users/api/internal/config" + "app/users/rpc/pb" +) + +type ServiceContext struct { + Config config.Config + UserRpc pb.UsercenterClient // RPC 客户端 +} + +func NewServiceContext(c config.Config) *ServiceContext { + userRpcClient := zrpc.MustNewClient(c.UserRpc) + return &ServiceContext{ + Config: c, + UserRpc: pb.NewUsercenterClient(userRpcClient.Conn()), + } +} +``` + +配置文件 `app/users/api/etc/user-api.yaml`: + +```yaml +Name: user-api +Host: 0.0.0.0 +Port: 8888 + +# RPC 配置(使用 K8s DNS) +UserRpc: + Endpoints: + - user-rpc-svc.juwan.svc.cluster.local:50051 + +Database: + DataSource: postgres://user:pass@pg-dx:5432/juwan +``` + +#### 4. RPC 不在 Envoy 中配置路由 + +❌ **不要**在 `envoy-gateway.yaml` 中添加 RPC 集群: + +```yaml +# ❌❌❌ 不要这样做 ❌❌❌ +# routes: +# - match: +# prefix: /juwan.pb.Usercenter/ +# route: +# cluster: user_rpc_cluster # 这样会暴露 RPC +``` + +--- + +## 3️⃣ JWT 认证与分级访问控制 + +### 实现逻辑 + +``` +请求到达 Envoy → JWT 验证 + +┌──────────────────────────────────────┐ +│ 无效或缺省 Token │ +└──────────┬───────────────────────────┘ + │ + 公开路由 (允许) ──→ /api/users/login + /api/users/register + /api/products (列表、详情) + + 受保护路由 (拒绝) ──→ 返回 401 Unauthorized + │ + ┌──────▼───────────────────────────┐ + │ 有效 Token │ + │ (已登录) │ + └──────┬───────────────────────────┘ + │ + ┌──────▼──────────────────────────────────────────────┐ + │ 完整数据访问 (取决于后端 RPC) │ + │ - 用户信息 (包括隐私信息) │ + │ - 订单历史 │ + │ - 收藏列表 │ + └──────────────────────────────────────────────────────┘ +``` + +### 配置步骤 + +#### 1. 生成 JWT 密钥并存储为 K8s Secret + +```bash +# 生成 HMAC 密钥(用于签名) +openssl rand -hex 32 + +# 输出示例: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 + +# 创建 Secret +kubectl create secret generic jwt-secret \ + --from-literal=key=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \ + -n juwan + +# 验证 +kubectl get secret jwt-secret -n juwan -o yaml +``` + +#### 2. 更新 Envoy 配置(添加 JWT 验证) + +编辑 `deploy/k8s/envoy-gateway.yaml`: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: envoy-config + namespace: juwan +data: + envoy.yaml: | + static_resources: + listeners: + - name: listener_http + address: + socket_address: + address: 0.0.0.0 + port_value: 8080 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + http_filters: + # JWT 认证过滤器(在 router 之前) + - name: envoy.filters.http.jwt_authn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + + # 定义 JWT 提供者 + providers: + my-provider: + issuer: "juwan" + audiences: "api" + # 使用文件系统上的密钥 + local_jwks: + filename: /etc/envoy/jwks.json + + # 定义受保护的路由 + rules: + # 规则1: 登录和注册不需要认证 + - match: + prefix: /api/users/login + allow_missing_or_failed: true # 允许缺省/失败的 token + - match: + prefix: /api/users/register + allow_missing_or_failed: true + # 规则2: 重定向认证失败请求 + - match: + prefix: "/" + requires: + provider_name: "my-provider" + # 如果认证失败,Envoy 直接拒绝,返回 401 + + # 路由过滤器 + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + route_config: + name: local_route + virtual_hosts: + - name: backend + domains: ["*"] + routes: + - match: + prefix: /api/users + route: + cluster: user_api_cluster + - match: + prefix: /api/products + route: + cluster: product_api_cluster + # ... 其他路由 ... + + # ... clusters 定义保持不变 ... +``` + +#### 3. 在 API 服务中添加认证中间件 + +创建 `app/users/api/internal/middleware/authmiddleware.go`: + +```go +package middleware + +import ( + "fmt" + "net/http" + "strings" + + "github.com/golang-jwt/jwt/v4" +) + +type AuthMiddleware struct { + JwtSecret string +} + +func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 获取 Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + // 如果认证失败,Envoy 会返回 401, + // 但我们可以在 API 层添加自定义逻辑 + next(w, r) + return + } + + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + http.Error(w, "Invalid authorization header", http.StatusUnauthorized) + return + } + + token := parts[1] + + // 验证 token + _, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(m.JwtSecret), nil + }) + + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // Token 有效,继续处理 + next(w, r) + } +} +``` + +#### 4. 登录端点返回 JWT Token + +编辑 `app/users/api/internal/logic/user/loginlogic.go`: + +```go +package user + +import ( + "context" + "time" + "github.com/golang-jwt/jwt/v4" + "app/users/api/internal/svc" + "app/users/api/internal/types" +) + +type LoginLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginResp, error) { + // TODO: 验证用户名和密码 + + // 生成 JWT Token + claims := jwt.MapClaims{ + "userId": 1, // 实际应从数据库获取 + "username": req.Username, + "exp": time.Now().Add(24 * time.Hour).Unix(), + "iat": time.Now().Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(l.svcCtx.Config.JwtSecret)) + if err != nil { + return nil, err + } + + return &types.LoginResp{ + Token: tokenString, + Expires: time.Now().Add(24 * time.Hour).Unix(), + }, nil +} +``` + +### JWT 认证时的分级访问 + +**后端 RPC 可处理分级访问:** + +```go +// 在 User RPC 中实现 +func (s *UsercenterServer) GetUserInfo(ctx context.Context, req *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { + // 获取请求者的 userId(从 context 中取,由 API 层传递) + requesterID := ctx.Value("userId").(int64) + targetID := req.Id + + // 查询用户信息 + user := s.getUserFromDB(targetID) + + if requesterID == targetID { + // 自己查看自己 → 返回完整信息(包含隐私信息) + return &pb.GetUsersByIdResp{ + Users: &pb.Users{ + UserId: user.UserId, + Username: user.Username, + Email: user.Email, // ✅ 包含 + Phone: user.Phone, // ✅ 包含 + // ... 所有字段 + }, + }, nil + } else { + // 查看别人 → 返回部分信息 + return &pb.GetUsersByIdResp{ + Users: &pb.Users{ + UserId: user.UserId, + Username: user.Username, + // ❌ 不返回 Email、Phone 等隐私信息 + }, + }, nil + } +} +``` + +**或在 API 层处理:** + +```go +func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (*types.UserInfo, error) { + // 从 context 获取当前认证用户 + currentUser := l.ctx.Value("userId").(int64) + + // 调用 RPC + rpcResp, err := l.svcCtx.UserRpc.GetUsersById(l.ctx, &pb.GetUsersByIdReq{ + Id: req.UserId, + }) + + if currentUser == req.UserId { + // 自己查看自己 → 返回所有信息 + return &types.UserInfo{ + UserId: rpcResp.Users.UserId, + Username: rpcResp.Users.Username, + Email: rpcResp.Users.Email, + Phone: rpcResp.Users.Phone, + }, nil + } else { + // 查看别人 → 仅返回公开信息 + return &types.UserInfo{ + UserId: rpcResp.Users.UserId, + Username: rpcResp.Users.Username, + }, nil + } +} +``` + +--- + +## 4️⃣ 认证失败处理策略 + +### 当前配置 + +| 路由 | 认证要求 | 认证失败 | 示例 | +|-----|---------|--------|------| +| `/api/users/login` | ❌ 不需要 | 放行 | `POST /api/users/login` (允许) | +| `/api/users/register` | ❌ 不需要 | 放行 | `POST /api/users/register` (允许) | +| `/api/users/:userId` | ✅ 需要 | 拒绝 401 | `GET /api/users/123` (无 token → 401) | +| `/api/users/:userId/password` | ✅ 需要 | 拒绝 401 | `PUT /api/users/123/password` (无 token → 401) | +| `/api/products` | ❌ 不需要 | 放行 | `GET /api/products` (允许) | +| `/api/products/:id` | ❌ 不需要 | 放行 | `GET /api/products/1` (允许) | + +### 自定义错误响应 + +创建 `deploy/k8s/envoy-gateway.yaml` 的自定义拒绝响应: + +```yaml +# 在 jwt_authn 过滤器中配置 +http_filters: + - name: envoy.filters.http.jwt_authn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + providers: + my-provider: + issuer: "juwan" + audiences: "api" + local_jwks: + filename: /etc/envoy/jwks.json + rules: + - match: + prefix: /api/users/login + allow_missing_or_failed: true + - match: + prefix: /api/users/register + allow_missing_or_failed: true + - match: + prefix: / + requires: + provider_name: "my-provider" + # 认证失败后的行为 + bypass_cors_preflight: false # 允许 OPTIONS 跨域请求 +``` + +**认证失败时 Envoy 返回 401:** + +```json +HTTP/1.1 401 Unauthorized +content-type: text/html + +Jwt verification fails. +``` + +### 在 API 层添加自定义错误处理 + +如果希望自定义错误响应,可以在每个受保护的 handler 中添加检查: + +```go +func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (*types.UpdateUserInfoResp, error) { + // 获取当前请求者的身份 + ctx := context.WithValue(l.ctx, "currentUserId", req.UserId) + + // 如果没有匹配的 token,API 层可以添加自定义错误 + if req.UserId <= 0 { + return nil, errors.New("invalid user id") + } + + // 继续处理 + return &types.UpdateUserInfoResp{Message: "更新成功"}, nil +} +``` + +--- + +## 5️⃣ 完整工作流示例 + +### 场景:用户注册 → 登录 → 获取用户信息 + +#### 步骤 1: 注册用户(无需认证) + +```bash +curl -X POST http://localhost/api/users/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "john_doe", + "password": "securePass123", + "email": "john@example.com", + "phone": "13800138001" + }' + +# 响应 +{ + "userId": 1, + "username": "john_doe", + "email": "john@example.com", + "message": "用户注册成功" +} +``` + +#### 步骤 2: 登录获取 Token(无需认证) + +```bash +curl -X POST http://localhost/api/users/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "john_doe", + "password": "securePass123" + }' + +# 响应 +{ + "userId": 1, + "username": "john_doe", + "email": "john@example.com", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires": 1708694400 +} +``` + +#### 步骤 3: 获取用户信息(需要 Token) + +```bash +# ❌ 无 Token → 401 Unauthorized +curl http://localhost/api/users/1 +# Jwt verification fails. + +# ✅ 有 Token → 成功 +curl http://localhost/api/users/1 \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + +# 响应 +{ + "userId": 1, + "username": "john_doe", + "email": "john@example.com", + "phone": "13800138001", + "avatar": "https://...", + "status": 1, + "createAt": 1708608000, + "updateAt": 1708608000 +} +``` + +#### 步骤 4: 查看他人信息(部分数据) + +```bash +# 用户1 查看用户2 的信息(已登录) +curl http://localhost/api/users/2 \ + -H "Authorization: Bearer eyJhbGc..." + +# 响应(仅公开信息) +{ + "userId": 2, + "username": "jane_doe", + # ❌ 不包含 email, phone 等隐私信息 +} +``` + +--- + +## 6️⃣ 部署检查清单 + +在部署新服务或更新配置前,使用此清单: + +- [ ] **API 定义** - `desc/api/*.api` 文件已创建 +- [ ] **RPC 定义** - `desc/rpc/*.proto` 文件已创建(如需内部通信) +- [ ] **代码生成** - 运行 `goctl api/rpc` 命令生成代码 +- [ ] **业务逻辑** - 编辑 `app/*/api/internal/logic/` 实现功能 +- [ ] **K8s 部署清单** - 创建 `deploy/k8s/service/*/` 文件 + - [ ] ConfigMap(配置) + - [ ] Deployment(部署) + - [ ] Service(K8s 服务发现) + - [ ] NetworkPolicy(网络隔离,可选) +- [ ] **Envoy 更新** - 修改 `deploy/k8s/envoy-gateway.yaml` + - [ ] 添加路由规则 + - [ ] 添加上游集群 + - [ ] 验证健康检查地址 +- [ ] **测试验证** + ```bash + kubectl apply -f deploy/k8s/service/{name}/ + kubectl apply -f deploy/k8s/envoy-gateway.yaml + kubectl delete pods -n juwan -l app=envoy-gateway + curl http://localhost/api/{path} # 端口转发后测试 + ``` + +--- + +## 7️⃣ 故障排查 + +### 问题 1: 新服务无法访问 + +```bash +# 检查 Pod 状态 +kubectl get pods -n juwan + +# 查看 Pod 日志 +kubectl logs -n juwan -l app=your-service --tail=100 + +# 检查 Service +kubectl get svc -n juwan + +# 测试 DNS 解析 +kubectl exec -it {pod-name} -n juwan -- nslookup your-service-svc.juwan.svc.cluster.local +``` + +### 问题 2: Envoy 配置错误 + +```bash +# 查看 Envoy Pod 日志 +kubectl logs -n juwan -l app=envoy-gateway --tail=50 + +# 常见错误 +# - "no such field" → YAML 字段名与 Envoy 版本不兼容 +# - "Unknown cluster" → Envoy 配置中缺少 cluster 定义 +# - "Connection refused" → 后端服务未启动或 DNS 无法解析 +``` + +### 问题 3: JWT 认证失败 + +```bash +# 检查 JWT 配置是否正确 +kubectl get configmap envoy-config -n juwan -o yaml + +# 查看 jwks.json 是否存在 +kubectl exec -it {envoy-pod} -n juwan -- cat /etc/envoy/jwks.json + +# 验证 Token 格式 +curl -H "Authorization: Bearer {token}" http://localhost/api/users/1 +``` + +--- + +## 文件组织总结 + +``` +project-root/ +├── desc/ # 接口定义 +│ ├── api/ +│ │ ├── users.api # User API 定义 +│ │ └── product.api # ← 新增:Product API 定义 +│ ├── rpc/ +│ │ ├── users.proto # User RPC 定义 +│ │ └── product.proto # ← 新增:Product RPC 定义 +│ └── sql/ +│ +├── app/ # 应用代码 +│ ├── users/ +│ │ ├── api/ # User API 实现 +│ │ └── rpc/ # User RPC 实现(内部) +│ └── product/ # ← 新增:Product 服务 +│ ├── api/ +│ └── rpc/ +│ +├── deploy/ # K8s 部署 +│ ├── k8s/ +│ │ ├── envoy-gateway.yaml # ← 更新:添加新路由 +│ │ ├── base/ +│ │ └── service/ +│ │ ├── user/ +│ │ │ ├── user-api.yaml +│ │ │ └── user-rpc.yaml +│ │ └── product/ # ← 新增:Product 部署文件 +│ │ ├── product-api.yaml +│ │ └── product-rpc.yaml +│ └── envoy/ # 参考配置 +│ +└── docs/ # 文档 + └── PROJECT_GUIDE.md # ← 本文件 +``` + +--- + +希望这份指南能帮助你快速上手项目!有任何问题欢迎提出 📝 diff --git a/docs/README.md b/docs/README.md index 5a7909a..9cec6a9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,108 +1,108 @@ -# Envoy Gateway Configuration - -This document explains how the Envoy unified ingress gateway is configured and how to modify it. - -## Files - -- deploy/k8s/envoy/envoy.yaml: ConfigMap + Deployment + Service for Envoy - -## Current Behavior - -- Envoy listens on port 8080 in the Pod and exposes port 80 via a ClusterIP Service. -- Route `/api/users` to `user-api-svc:8888`. -- Route `/api/email` to `email-api-svc:8888`. -- Route `/healthz` returns `200 ok` directly from gateway. -- Unknown routes return `404` from gateway. - -## Routing - -In envoy.yaml, routes are defined under: - -static_resources -> listeners -> http_connection_manager -> route_config -> virtual_hosts - -The current routing rules are: - -- `prefix: /api/users` -> `cluster: user_api_cluster` -- `prefix: /api/email` -> `cluster: email_api_cluster` -- `path: /healthz` -> direct response `200` -- `prefix: /` -> direct response `404` - -To add a new HTTP service, add a new route above the default route and define a new cluster. - -Example: route `/api/order` to `order-api-svc:8899` - -1) Add a route match: - -- match: - prefix: "/api/order" - route: - cluster: order_api_cluster - -1) Add a cluster: - -- name: order_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: order_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: order-api-svc.juwan.svc.cluster.local - port_value: 8899 - -## CSRF Protection (Double Cookie) - -Envoy uses a Lua filter for double-cookie CSRF validation: - -- Safe methods (GET/HEAD/OPTIONS): - - If missing, Envoy auto-issues two cookies: - - `csrf_token` - - `csrf_guard` -- Unsafe methods (POST/PUT/PATCH/DELETE, etc): - - Requires BOTH headers: - - `X-CSRF-Token` - - `X-CSRF-Guard` - - Requires BOTH cookies: - - `csrf_token` - - `csrf_guard` - - Header values must exactly match cookie values, otherwise Envoy returns `403`. - -If you want different cookie or header names, update these constants in Lua: - -- `TOKEN_COOKIE` -- `GUARD_COOKIE` -- `TOKEN_HEADER` -- `GUARD_HEADER` - -To relax or tighten rules, edit the functions: - -- is_safe(method) -- envoy_on_request(request_handle) - -## Cookie Attributes - -Current Set-Cookie: - -- `csrf_token=; Path=/; SameSite=Strict` -- `csrf_guard=; Path=/; SameSite=Strict` - -## Deployment - -Apply or update: - -kubectl apply -f deploy/k8s/envoy/envoy.yaml - -## Common Changes - -- Change listening port: - - Update listener port_value and Service targetPort/port. -- Change service namespace: - - Update cluster DNS addresses (e.g. `service.ns.svc.cluster.local`). -- Add more services: - - Add route + add cluster, as shown above. -- Update CSRF policy: - - Edit Lua validation logic in `envoy.filters.http.lua`. +# Envoy Gateway Configuration + +This document explains how the Envoy unified ingress gateway is configured and how to modify it. + +## Files + +- deploy/k8s/envoy/envoy.yaml: ConfigMap + Deployment + Service for Envoy + +## Current Behavior + +- Envoy listens on port 8080 in the Pod and exposes port 80 via a ClusterIP Service. +- Route `/api/users` to `user-api-svc:8888`. +- Route `/api/email` to `email-api-svc:8888`. +- Route `/healthz` returns `200 ok` directly from gateway. +- Unknown routes return `404` from gateway. + +## Routing + +In envoy.yaml, routes are defined under: + +static_resources -> listeners -> http_connection_manager -> route_config -> virtual_hosts + +The current routing rules are: + +- `prefix: /api/users` -> `cluster: user_api_cluster` +- `prefix: /api/email` -> `cluster: email_api_cluster` +- `path: /healthz` -> direct response `200` +- `prefix: /` -> direct response `404` + +To add a new HTTP service, add a new route above the default route and define a new cluster. + +Example: route `/api/order` to `order-api-svc:8899` + +1) Add a route match: + +- match: + prefix: "/api/order" + route: + cluster: order_api_cluster + +1) Add a cluster: + +- name: order_api_cluster + connect_timeout: 2s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: order_api_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: order-api-svc.juwan.svc.cluster.local + port_value: 8899 + +## CSRF Protection (Double Cookie) + +Envoy uses a Lua filter for double-cookie CSRF validation: + +- Safe methods (GET/HEAD/OPTIONS): + - If missing, Envoy auto-issues two cookies: + - `csrf_token` + - `csrf_guard` +- Unsafe methods (POST/PUT/PATCH/DELETE, etc): + - Requires BOTH headers: + - `X-CSRF-Token` + - `X-CSRF-Guard` + - Requires BOTH cookies: + - `csrf_token` + - `csrf_guard` + - Header values must exactly match cookie values, otherwise Envoy returns `403`. + +If you want different cookie or header names, update these constants in Lua: + +- `TOKEN_COOKIE` +- `GUARD_COOKIE` +- `TOKEN_HEADER` +- `GUARD_HEADER` + +To relax or tighten rules, edit the functions: + +- is_safe(method) +- envoy_on_request(request_handle) + +## Cookie Attributes + +Current Set-Cookie: + +- `csrf_token=; Path=/; SameSite=Strict` +- `csrf_guard=; Path=/; SameSite=Strict` + +## Deployment + +Apply or update: + +kubectl apply -f deploy/k8s/envoy/envoy.yaml + +## Common Changes + +- Change listening port: + - Update listener port_value and Service targetPort/port. +- Change service namespace: + - Update cluster DNS addresses (e.g. `service.ns.svc.cluster.local`). +- Add more services: + - Add route + add cluster, as shown above. +- Update CSRF policy: + - Edit Lua validation logic in `envoy.filters.http.lua`. diff --git a/docs/deployment-troubleshooting.md b/docs/deployment-troubleshooting.md index 555b5ef..eec8a7e 100644 --- a/docs/deployment-troubleshooting.md +++ b/docs/deployment-troubleshooting.md @@ -1,385 +1,385 @@ -# Kubernetes 部署问题排查与解决记录 - -**日期**: 2026年2月23日 -**问题**: user-rpc 和 Redis 部署失败 -**状态**: 已诊断,解决中 - ---- - -## 📋 问题描述 - -执行 `kubectl apply -f test.yaml` 后,资源虽然创建成功,但实际的应用 pods 并未正常运行: - -``` -kubectl apply -f ..\test.yaml -✓ deployment.apps/user-rpc created -✓ service/user-rpc-svc created -✓ horizontalpodautoscaler.autoscaling/user-rpc-hpa-c created -✓ horizontalpodautoscaler.autoscaling/user-rpc-hpa-m created -✓ redisreplication.redis.redis.opstreelabs.in/user-redis created -✓ redissentinel.redis.redis.opstreelabs.in/user-redis-sentinel created -✓ cluster.postgresql.cnpg.io/user-db created -``` - -但执行 `kubectl get all` 后,发现: -- ❌ **user-rpc pods 未创建**(Deployment 0/3 replicas ready) -- ❌ **Redis pods 未创建**(RedisReplication 资源存在但无 pods) -- ✅ user-db pods 正常运行(3/3) - ---- - -## 🔍 排查过程 - -### 第一步:检查 Deployment 状态 - -```bash -kubectl describe deployment user-rpc -``` - -**发现**: -``` -Conditions: - Type Status Reason - ---- ------ ------ - Progressing True NewReplicaSetCreated - Available False MinimumReplicasUnavailable - ReplicaFailure True FailedCreate -``` - -### 第二步:检查 ReplicaSet 详情 - -```bash -kubectl describe replicaset user-rpc-6bf77fbcd9 -``` - -**发现关键错误**: -``` -Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Warning FailedCreate 3m53s replicaset-controller Error creating: - pods "user-rpc-6bf77fbcd9-" is forbidden: error looking up service - account default/find-endpoints: serviceaccount "find-endpoints" not found -``` - -**问题 #1 诊断完成**:❌ **缺失 ServiceAccount "find-endpoints"** - -### 第三步:检查现有 ServiceAccounts - -```bash -kubectl get serviceaccount -``` - -**结果**: -``` -NAME AGE -cluster-example 4d10h -default 13d -redis-operator 9h -user-db 4m9s -``` - -确认 `find-endpoints` 不存在。 - -### 第四步:检查 Secrets - -```bash -kubectl get secrets -``` - -**结果**:默认 secrets 都存在,包括: -- ✅ user-db-app -- ✅ user-redis -- ✅ user-db-ca, user-db-replication, user-db-server - -### 第五步:检查 Redis 部署 - -```bash -kubectl get redisreplication -kubectl get pods | grep redis -``` - -**发现**: -- ✅ RedisReplication 资源存在 -- ❌ Redis pods **完全没有被创建** - -**问题 #2 诊断**:❌ **Redis Operator 未响应 RedisReplication 资源** - ---- - -## 🔧 第一次修复尝试 - -### 创建缺失的 ServiceAccount - -```bash -kubectl create serviceaccount find-endpoints -``` - -**结果**:✅ ServiceAccount 创建成功 - -### 重启 Deployment - -```bash -kubectl rollout restart deployment user-rpc -``` - -**等待 5-10 秒后重新检查**: - -```bash -kubectl get pods -o wide -``` - -**新的发现**: - -``` -NAME READY STATUS RESTARTS AGE -user-rpc-66f97fbdcc-ws7rc 0/1 ErrImagePull 0 26s -user-rpc-6bf77fbcd9-njm2z 0/1 ErrImagePull 0 29s -user-rpc-6bf77fbcd9-nwjtw 0/1 ImagePullBackOff 0 29s -user-rpc-6bf77fbcd9-wjrf8 0/1 ErrImagePull 0 29s -``` - -✅ **好消息**:Pods 现在被创建了!(说明 ServiceAccount 问题已解决) -❌ **新问题**:镜像拉取失败 - ---- - -## 🐛 根因分析 - -### 问题 #1:缺失 ServiceAccount ✅ 已解决 - -**根本原因**:test.yaml 的 Deployment manifest 指定了: -```yaml -spec: - template: - spec: - serviceAccountName: find-endpoints # 这个 ServiceAccount 不存在 -``` - -但没有在 test.yaml 中创建 ServiceAccount 资源。 - -**解决方案**: -```bash -kubectl create serviceaccount find-endpoints -``` - -或在 test.yaml 中添加: -```yaml ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: find-endpoints - namespace: default -``` - ---- - -### 问题 #2:镜像拉取失败 ❌ 需要修复 - -```bash -kubectl describe pod user-rpc-6bf77fbcd9-njm2z -``` - -**详细错误日志**: - -``` -Events: - Warning Failed 38s kubelet Failed to pull image - "103.236.53.208:4418/library/user-rpc@sha256:76b27d3eb4d5d44e...": - Error response from daemon: Get "https://103.236.53.208:4418/v2/": - context deadline exceeded (Client.Timeout exceeded while awaiting headers) - - Warning Failed 23s kubelet Failed to pull image - "103.236.53.208:4418/library/user-rpc@sha256:76b27d3eb4d5d44e...": - http: server gave HTTP response to HTTPS client -``` - -**根本原因分析**: - -1. **网络连接失败**:`context deadline exceeded` - 无法连接到镜像仓库 -2. **协议不匹配**:`http: server gave HTTP response to HTTPS client` - - - 地址 `103.236.53.208:4418` 应该是 HTTP 而不是 HTTPS - - Docker daemon 尝试用 HTTPS 连接,但服务器使用 HTTP - -**可能原因**: -- 镜像仓库地址错误或不可访问 -- 镜像仓库需要特定的网络配置 -- 仓库服务器离线或配置不当 - ---- - -### 问题 #3:Redis 部署失败 ❌ 需要诊断 - -**现象**: -- RedisReplication 和 RedisSentinel CRD 资源创建成功 -- 但没有对应的 Redis pods 被创建 -- `kubectl get pods | grep redis` 返回空 - -**可能原因**: - -1. **Redis Operator 未正常工作** - - Operator pod 可能存在问题 - - Operator 未能监听到新的 RedisReplication 资源 - -2. **CRD 或 API 版本问题** - - manifest 中使用的 API 版本 `v1beta2` 可能不匹配 Operator 版本 - -3. **资源限制或权限问题** - - Operator 无权限创建 pods - - 集群资源限制阻止了 pod 创建 - ---- - -## ✅ 已执行的修复 - -| # | 问题 | 修复方法 | 状态 | -|---|------|---------|------| -| 1 | 缺失 ServiceAccount | `kubectl create serviceaccount find-endpoints` | ✅ 完成 | -| 2 | 镜像拉取失败 | 需要更新镜像地址或修复网络 | ⏳ 待处理 | -| 3 | Redis pods 未创建 | 需要诊断 Operator 日志 | ⏳ 待诊断 | - ---- - -## 🚀 下一步解决方案 - -### 优先级 1:修复 user-rpc 镜像拉取 - -**选项 A:使用本地/内部镜像** -```yaml -# 修改 test.yaml 中的镜像地址 -image: localhost:5000/user-rpc:latest # 本地私有仓库 -# 或 -image: user-rpc:latest # 本地镜像(如果已通过 docker load 导入) -``` - -**选项 B:修复仓库地址** -```yaml -# 如果 103.236.53.208:4418 确实是正确仓库 -image: http://103.236.53.208:4418/library/user-rpc:latest # 显式使用 HTTP -``` - -**验证步骤**: -```bash -# 检查镜像仓库连接性 -curl -v http://103.236.53.208:4418/v2/ -``` - -### 优先级 2:诊断 Redis Operator - -```bash -# 查看 Operator 日志 -kubectl logs -l app.kubernetes.io/name=redis-operator -f - -# 查看 Operator pod -kubectl get pods -A | grep redis-operator - -# 查看 RedisReplication 详细信息 -kubectl describe redisreplication user-redis - -# 检查 Operator 权限(RBAC) -kubectl get role,rolebinding,clusterrole,clusterrolebinding | grep redis -``` - -### 优先级 3:增强 test.yaml - -建议在 test.yaml 中添加缺失的资源定义: - -```yaml ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: find-endpoints - namespace: default - ---- -apiVersion: v1 -kind: Secret -metadata: - name: registry-credentials - namespace: default -type: kubernetes.io/dockercfg -data: - .dockercfg: # 如果需要私有仓库认证 -``` - ---- - -## 📊 当前集群状态 - -### Pods 状态总结 - -| 应用 | 期望副本 | 实际运行 | 状态 | -|------|---------|---------|------| -| user-db | 3 | 3 | ✅ 正常 | -| user-rpc | 3 | 0 | ❌ 镜像拉取失败 | -| Redis | 3 | 0 | ❌ Operator 未创建 | -| Sentinel | 3 | 0 | ❌ Operator 未创建 | - -### Services 状态 - -``` -✅ kubernetes (内置) -✅ user-rpc-svc:9001 -✅ user-db-r:5432 (只读副本) -✅ user-db-ro:5432 (只读副本) -✅ user-db-rw:5432 (读写主副本) -``` - -### HPA 配置 - -``` -✅ user-rpc-hpa-c (CPU 目标: 80%) - 无法工作(pods 未运行) -✅ user-rpc-hpa-m (Memory 目标: 80%) - 无法工作(pods 未运行) -``` - ---- - -## 📝 关键命令速查表 - -```bash -# 查看 Deployment 状态 -kubectl describe deployment user-rpc - -# 查看 ReplicaSet 错误事件 -kubectl describe replicaset user-rpc-6bf77fbcd9 - -# 查看 Pod 详细错误 -kubectl describe pod user-rpc-6bf77fbcd9-njm2z - -# 查看 Pod 日志 -kubectl logs user-rpc-6bf77fbcd9-njm2z - -# 查看所有事件(按时间排序) -kubectl get events --sort-by='.lastTimestamp' - -# 查看特定命名空间的所有资源 -kubectl get all -n default - -# 重新启动 deployment(强制重新创建 pods) -kubectl rollout restart deployment user-rpc - -# 查看 Operator 日志 -kubectl logs -l app.kubernetes.io/name=redis-operator - -# 检查 CRD 注册状态 -kubectl api-resources | grep redis -``` - ---- - -## 🎯 总结 - -| 问题 | 原因 | 解决状态 | -|------|------|---------| -| **ServiceAccount 缺失** | manifest 中声明但未创建 | ✅ **已解决** | -| **镜像拉取失败** | 仓库地址不可达或协议不匹配 | ⏳ **待处理** | -| **Redis 未部署** | Operator 未响应 CRD | ⏳ **待诊断** | - -**建议行动**: -1. 确认/修复 user-rpc 镜像地址 -2. 诊断 Redis Operator 状态 -3. 验证所有依赖的 ServiceAccounts 和 Secrets 是否存在 -4. 考虑在 test.yaml 中添加完整的资源定义,避免手工创建 - +# Kubernetes 部署问题排查与解决记录 + +**日期**: 2026年2月23日 +**问题**: user-rpc 和 Redis 部署失败 +**状态**: 已诊断,解决中 + +--- + +## 📋 问题描述 + +执行 `kubectl apply -f test.yaml` 后,资源虽然创建成功,但实际的应用 pods 并未正常运行: + +``` +kubectl apply -f ..\test.yaml +✓ deployment.apps/user-rpc created +✓ service/user-rpc-svc created +✓ horizontalpodautoscaler.autoscaling/user-rpc-hpa-c created +✓ horizontalpodautoscaler.autoscaling/user-rpc-hpa-m created +✓ redisreplication.redis.redis.opstreelabs.in/user-redis created +✓ redissentinel.redis.redis.opstreelabs.in/user-redis-sentinel created +✓ cluster.postgresql.cnpg.io/user-db created +``` + +但执行 `kubectl get all` 后,发现: +- ❌ **user-rpc pods 未创建**(Deployment 0/3 replicas ready) +- ❌ **Redis pods 未创建**(RedisReplication 资源存在但无 pods) +- ✅ user-db pods 正常运行(3/3) + +--- + +## 🔍 排查过程 + +### 第一步:检查 Deployment 状态 + +```bash +kubectl describe deployment user-rpc +``` + +**发现**: +``` +Conditions: + Type Status Reason + ---- ------ ------ + Progressing True NewReplicaSetCreated + Available False MinimumReplicasUnavailable + ReplicaFailure True FailedCreate +``` + +### 第二步:检查 ReplicaSet 详情 + +```bash +kubectl describe replicaset user-rpc-6bf77fbcd9 +``` + +**发现关键错误**: +``` +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Warning FailedCreate 3m53s replicaset-controller Error creating: + pods "user-rpc-6bf77fbcd9-" is forbidden: error looking up service + account default/find-endpoints: serviceaccount "find-endpoints" not found +``` + +**问题 #1 诊断完成**:❌ **缺失 ServiceAccount "find-endpoints"** + +### 第三步:检查现有 ServiceAccounts + +```bash +kubectl get serviceaccount +``` + +**结果**: +``` +NAME AGE +cluster-example 4d10h +default 13d +redis-operator 9h +user-db 4m9s +``` + +确认 `find-endpoints` 不存在。 + +### 第四步:检查 Secrets + +```bash +kubectl get secrets +``` + +**结果**:默认 secrets 都存在,包括: +- ✅ user-db-app +- ✅ user-redis +- ✅ user-db-ca, user-db-replication, user-db-server + +### 第五步:检查 Redis 部署 + +```bash +kubectl get redisreplication +kubectl get pods | grep redis +``` + +**发现**: +- ✅ RedisReplication 资源存在 +- ❌ Redis pods **完全没有被创建** + +**问题 #2 诊断**:❌ **Redis Operator 未响应 RedisReplication 资源** + +--- + +## 🔧 第一次修复尝试 + +### 创建缺失的 ServiceAccount + +```bash +kubectl create serviceaccount find-endpoints +``` + +**结果**:✅ ServiceAccount 创建成功 + +### 重启 Deployment + +```bash +kubectl rollout restart deployment user-rpc +``` + +**等待 5-10 秒后重新检查**: + +```bash +kubectl get pods -o wide +``` + +**新的发现**: + +``` +NAME READY STATUS RESTARTS AGE +user-rpc-66f97fbdcc-ws7rc 0/1 ErrImagePull 0 26s +user-rpc-6bf77fbcd9-njm2z 0/1 ErrImagePull 0 29s +user-rpc-6bf77fbcd9-nwjtw 0/1 ImagePullBackOff 0 29s +user-rpc-6bf77fbcd9-wjrf8 0/1 ErrImagePull 0 29s +``` + +✅ **好消息**:Pods 现在被创建了!(说明 ServiceAccount 问题已解决) +❌ **新问题**:镜像拉取失败 + +--- + +## 🐛 根因分析 + +### 问题 #1:缺失 ServiceAccount ✅ 已解决 + +**根本原因**:test.yaml 的 Deployment manifest 指定了: +```yaml +spec: + template: + spec: + serviceAccountName: find-endpoints # 这个 ServiceAccount 不存在 +``` + +但没有在 test.yaml 中创建 ServiceAccount 资源。 + +**解决方案**: +```bash +kubectl create serviceaccount find-endpoints +``` + +或在 test.yaml 中添加: +```yaml +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: find-endpoints + namespace: default +``` + +--- + +### 问题 #2:镜像拉取失败 ❌ 需要修复 + +```bash +kubectl describe pod user-rpc-6bf77fbcd9-njm2z +``` + +**详细错误日志**: + +``` +Events: + Warning Failed 38s kubelet Failed to pull image + "103.236.53.208:4418/library/user-rpc@sha256:76b27d3eb4d5d44e...": + Error response from daemon: Get "https://103.236.53.208:4418/v2/": + context deadline exceeded (Client.Timeout exceeded while awaiting headers) + + Warning Failed 23s kubelet Failed to pull image + "103.236.53.208:4418/library/user-rpc@sha256:76b27d3eb4d5d44e...": + http: server gave HTTP response to HTTPS client +``` + +**根本原因分析**: + +1. **网络连接失败**:`context deadline exceeded` - 无法连接到镜像仓库 +2. **协议不匹配**:`http: server gave HTTP response to HTTPS client` - + - 地址 `103.236.53.208:4418` 应该是 HTTP 而不是 HTTPS + - Docker daemon 尝试用 HTTPS 连接,但服务器使用 HTTP + +**可能原因**: +- 镜像仓库地址错误或不可访问 +- 镜像仓库需要特定的网络配置 +- 仓库服务器离线或配置不当 + +--- + +### 问题 #3:Redis 部署失败 ❌ 需要诊断 + +**现象**: +- RedisReplication 和 RedisSentinel CRD 资源创建成功 +- 但没有对应的 Redis pods 被创建 +- `kubectl get pods | grep redis` 返回空 + +**可能原因**: + +1. **Redis Operator 未正常工作** + - Operator pod 可能存在问题 + - Operator 未能监听到新的 RedisReplication 资源 + +2. **CRD 或 API 版本问题** + - manifest 中使用的 API 版本 `v1beta2` 可能不匹配 Operator 版本 + +3. **资源限制或权限问题** + - Operator 无权限创建 pods + - 集群资源限制阻止了 pod 创建 + +--- + +## ✅ 已执行的修复 + +| # | 问题 | 修复方法 | 状态 | +|---|------|---------|------| +| 1 | 缺失 ServiceAccount | `kubectl create serviceaccount find-endpoints` | ✅ 完成 | +| 2 | 镜像拉取失败 | 需要更新镜像地址或修复网络 | ⏳ 待处理 | +| 3 | Redis pods 未创建 | 需要诊断 Operator 日志 | ⏳ 待诊断 | + +--- + +## 🚀 下一步解决方案 + +### 优先级 1:修复 user-rpc 镜像拉取 + +**选项 A:使用本地/内部镜像** +```yaml +# 修改 test.yaml 中的镜像地址 +image: localhost:5000/user-rpc:latest # 本地私有仓库 +# 或 +image: user-rpc:latest # 本地镜像(如果已通过 docker load 导入) +``` + +**选项 B:修复仓库地址** +```yaml +# 如果 103.236.53.208:4418 确实是正确仓库 +image: http://103.236.53.208:4418/library/user-rpc:latest # 显式使用 HTTP +``` + +**验证步骤**: +```bash +# 检查镜像仓库连接性 +curl -v http://103.236.53.208:4418/v2/ +``` + +### 优先级 2:诊断 Redis Operator + +```bash +# 查看 Operator 日志 +kubectl logs -l app.kubernetes.io/name=redis-operator -f + +# 查看 Operator pod +kubectl get pods -A | grep redis-operator + +# 查看 RedisReplication 详细信息 +kubectl describe redisreplication user-redis + +# 检查 Operator 权限(RBAC) +kubectl get role,rolebinding,clusterrole,clusterrolebinding | grep redis +``` + +### 优先级 3:增强 test.yaml + +建议在 test.yaml 中添加缺失的资源定义: + +```yaml +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: find-endpoints + namespace: default + +--- +apiVersion: v1 +kind: Secret +metadata: + name: registry-credentials + namespace: default +type: kubernetes.io/dockercfg +data: + .dockercfg: # 如果需要私有仓库认证 +``` + +--- + +## 📊 当前集群状态 + +### Pods 状态总结 + +| 应用 | 期望副本 | 实际运行 | 状态 | +|------|---------|---------|------| +| user-db | 3 | 3 | ✅ 正常 | +| user-rpc | 3 | 0 | ❌ 镜像拉取失败 | +| Redis | 3 | 0 | ❌ Operator 未创建 | +| Sentinel | 3 | 0 | ❌ Operator 未创建 | + +### Services 状态 + +``` +✅ kubernetes (内置) +✅ user-rpc-svc:9001 +✅ user-db-r:5432 (只读副本) +✅ user-db-ro:5432 (只读副本) +✅ user-db-rw:5432 (读写主副本) +``` + +### HPA 配置 + +``` +✅ user-rpc-hpa-c (CPU 目标: 80%) - 无法工作(pods 未运行) +✅ user-rpc-hpa-m (Memory 目标: 80%) - 无法工作(pods 未运行) +``` + +--- + +## 📝 关键命令速查表 + +```bash +# 查看 Deployment 状态 +kubectl describe deployment user-rpc + +# 查看 ReplicaSet 错误事件 +kubectl describe replicaset user-rpc-6bf77fbcd9 + +# 查看 Pod 详细错误 +kubectl describe pod user-rpc-6bf77fbcd9-njm2z + +# 查看 Pod 日志 +kubectl logs user-rpc-6bf77fbcd9-njm2z + +# 查看所有事件(按时间排序) +kubectl get events --sort-by='.lastTimestamp' + +# 查看特定命名空间的所有资源 +kubectl get all -n default + +# 重新启动 deployment(强制重新创建 pods) +kubectl rollout restart deployment user-rpc + +# 查看 Operator 日志 +kubectl logs -l app.kubernetes.io/name=redis-operator + +# 检查 CRD 注册状态 +kubectl api-resources | grep redis +``` + +--- + +## 🎯 总结 + +| 问题 | 原因 | 解决状态 | +|------|------|---------| +| **ServiceAccount 缺失** | manifest 中声明但未创建 | ✅ **已解决** | +| **镜像拉取失败** | 仓库地址不可达或协议不匹配 | ⏳ **待处理** | +| **Redis 未部署** | Operator 未响应 CRD | ⏳ **待诊断** | + +**建议行动**: +1. 确认/修复 user-rpc 镜像地址 +2. 诊断 Redis Operator 状态 +3. 验证所有依赖的 ServiceAccounts 和 Secrets 是否存在 +4. 考虑在 test.yaml 中添加完整的资源定义,避免手工创建 + diff --git a/docs/gozero-redis-configuration.md b/docs/gozero-redis-configuration.md index ae4e062..b8f0e0f 100644 --- a/docs/gozero-redis-configuration.md +++ b/docs/gozero-redis-configuration.md @@ -1,1497 +1,1497 @@ -# Go-Zero 框架 Redis 配置完全指南 - -**框架版本:** go-zero v1.5+ -**Redis 版本:** 7.0.12 -**部署环境:** Kubernetes (juwan namespace) -**文档日期:** 2026年2月22日 - ---- - -## 📋 目录 - -1. [配置概览](#配置概览) -2. [单节点模式](#单节点模式) -3. [Sentinel 哨兵模式](#sentinel-哨兵模式) -4. [集群模式](#集群模式) -5. [配置项详解](#配置项详解) -6. [代码实现](#代码实现) -7. [常用操作示例](#常用操作示例) -8. [高级特性](#高级特性) -9. [性能优化](#性能优化) -10. [故障排查](#故障排查) -11. [最佳实践](#最佳实践) - ---- - -## 🎯 配置概览 - -### Go-Zero Redis 支持的模式 - -| 模式 | Type 值 | 用途 | 高可用 | 推荐度 | -|-----|---------|------|--------|--------| -| **单节点** | `node` | 开发/测试 | ❌ | ⭐⭐ | -| **Sentinel** | `sentinel` | 生产环境 | ✅ | ⭐⭐⭐⭐⭐ | -| **集群** | `cluster` | 大规模分片 | ✅ | ⭐⭐⭐⭐ | - -### 配置文件位置 - -``` -app/users/rpc/ -├── etc/ -│ └── pb.yaml ← Redis 配置写在这里 -├── internal/ -│ ├── config/ -│ │ └── config.go ← 定义配置结构 -│ └── svc/ -│ └── serviceContext.go ← 初始化 Redis 客户端 -└── pb.go -``` - ---- - -## 🔵 单节点模式 - -### 适用场景 - -- ✅ 开发环境 -- ✅ 测试环境 -- ✅ POC 演示 -- ❌ 生产环境(无高可用) - -### 配置文件 - -**`app/users/rpc/etc/pb.yaml`** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# Redis 单节点配置 -Redis: - Host: user-redis-master.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} # 从环境变量读取 - # Db: 0 # 可选,默认 0 - # MaxIdle: 8 # 可选,连接池最大闲置连接数 - # MaxActive: 0 # 可选,连接池最大活跃连接数,0 表示无限制 - -Etcd: - Hosts: - - etcd-service.juwan.svc.cluster.local:2379 - Key: user.rpc -``` - -### Config 结构 - -**`app/users/rpc/internal/config/config.go`** -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf // Redis 配置 -} -``` - -### ServiceContext 初始化 - -**`app/users/rpc/internal/svc/serviceContext.go`** -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis -} - -func NewServiceContext(c config.Config) *ServiceContext { - return &ServiceContext{ - Config: c, - Redis: redis.MustNewRedis(c.Redis), // 初始化 Redis - } -} -``` - -### 使用示例 - -**`app/users/rpc/internal/logic/getUsersByIdLogic.go`** -```go -package logic - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "juwan-backend/app/users/rpc/internal/svc" - "juwan-backend/app/users/rpc/pb" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GetUsersByIdLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewGetUsersByIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUsersByIdLogic { - return &GetUsersByIdLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -func (l *GetUsersByIdLogic) GetUsersById(in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { - // 缓存 key - cacheKey := fmt.Sprintf("user:%d", in.Id) - - // 1. 尝试从缓存获取 - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - // 缓存命中 - var user pb.User - if err := json.Unmarshal([]byte(cached), &user); err == nil { - l.Logger.Infof("Cache hit for user:%d", in.Id) - return &pb.GetUsersByIdResp{User: &user}, nil - } - } - - // 2. 缓存未命中,从数据库查询 - l.Logger.Infof("Cache miss for user:%d, querying DB", in.Id) - user := l.fetchUserFromDB(in.Id) - if user == nil { - return nil, fmt.Errorf("user not found") - } - - // 3. 写入缓存(1小时过期) - userJSON, _ := json.Marshal(user) - err = l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - if err != nil { - l.Logger.Errorf("Failed to set cache: %v", err) - } - - return &pb.GetUsersByIdResp{User: user}, nil -} - -func (l *GetUsersByIdLogic) fetchUserFromDB(id int64) *pb.User { - // 实际从数据库查询 - // ... - return &pb.User{Id: id, Name: "John Doe"} -} -``` - ---- - -## 🟡 Sentinel 哨兵模式 - -### 适用场景 - -- ✅✅✅ **生产环境强烈推荐** -- ✅ 自动故障转移 -- ✅ 高可用架构 -- ✅ 主从自动切换 - -### 配置文件 - -**`app/users/rpc/etc/pb.yaml`** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# Redis Sentinel 配置(推荐生产环境使用) -Redis: - - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - Pass: ${REDIS_PASSWORD} - -# 或者使用完整配置 -# Redis: -# Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 -# Type: sentinel -# Pass: ${REDIS_PASSWORD} -# # Sentinel 特有配置 -# MasterName: mymaster # Sentinel 主节点名称,默认 mymaster - -Etcd: - Hosts: - - etcd-service.juwan.svc.cluster.local:2379 - Key: user.rpc -``` - -### Config 结构(同单节点) - -**`app/users/rpc/internal/config/config.go`** -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf // 支持所有模式 -} -``` - -### ServiceContext 初始化(同单节点) - -**`app/users/rpc/internal/svc/serviceContext.go`** -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis -} - -func NewServiceContext(c config.Config) *ServiceContext { - // go-zero 会根据 Type 自动选择连接模式 - return &ServiceContext{ - Config: c, - Redis: redis.MustNewRedis(c.Redis), - } -} -``` - -### Sentinel 配置详解 - -**完整配置选项:** -```yaml -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - Pass: ${REDIS_PASSWORD} - - # Sentinel 特有配置 - MasterName: mymaster # Sentinel 监控的主节点名称 - - # 连接池配置(可选) - MaxIdle: 8 # 最大闲置连接数 - MaxActive: 0 # 最大活跃连接数,0 表示无限制 - IdleTimeout: 300 # 闲置连接超时时间(秒) - - # 超时配置(可选) - ConnectTimeout: 5000 # 连接超时(毫秒) - ReadTimeout: 3000 # 读超时(毫秒) - WriteTimeout: 3000 # 写超时(毫秒) -``` - -### 优势说明 - -```go -// Sentinel 模式的自动故障处理流程: - -// 1. 应用连接到 Sentinel -app → Sentinel Service (26379) - -// 2. Sentinel 返回当前主节点地址 -Sentinel → app: "主节点在 10.244.1.10:6379" - -// 3. 应用连接到主节点进行读写 -app → Redis Master (10.244.1.10:6379) - -// 4. 主节点故障,Sentinel 检测到 -Redis Master (✗ 宕机) -Sentinel → 检测 → 投票 → 提升新主节点 - -// 5. 应用下次请求时自动连接到新主节点 -app → Sentinel → "新主节点在 10.244.2.20:6379" -app → New Redis Master (10.244.2.20:6379) - -// 整个过程应用无需重启,自动完成切换! -``` - ---- - -## 🔴 集群模式 - -### 适用场景 - -- ✅ 大规模数据(需要分片) -- ✅ 超高并发 -- ✅ 数据量超过单机内存 -- ⚠️ 配置和运维复杂度高 - -### 配置文件 - -**`app/users/rpc/etc/pb.yaml`** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# Redis Cluster 配置 -Redis: - - Host: redis-cluster-0.redis-cluster.juwan.svc.cluster.local:6379 - - Host: redis-cluster-1.redis-cluster.juwan.svc.cluster.local:6379 - - Host: redis-cluster-2.redis-cluster.juwan.svc.cluster.local:6379 - Type: cluster - Pass: ${REDIS_PASSWORD} - -Etcd: - Hosts: - - etcd-service.juwan.svc.cluster.local:2379 - Key: user.rpc -``` - -### Config 结构 - -**`app/users/rpc/internal/config/config.go`** -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf -} -``` - -### 集群模式特点 - -**数据分片:** -``` -应用请求 - ↓ -根据 key 计算 hash slot (0-16383) - ↓ -路由到对应的分片节点 - ↓ -┌─────────┬─────────┬─────────┐ -│ Shard 1 │ Shard 2 │ Shard 3 │ -│ 0-5460 │5461-10922│10923-16383│ -└─────────┴─────────┴─────────┘ -``` - -**注意事项:** -- ❌ 不支持多 key 操作(如 MGET, MSET)跨分片 -- ❌ 不支持事务(MULTI/EXEC)跨分片 -- ✅ 单 key 操作完全正常 -- ✅ 支持 hash tag 控制 key 分布 - ---- - -## 📚 配置项详解 - -### redis.RedisConf 完整配置 - -**结构定义:** -```go -type RedisConf struct { - Host string // Redis 地址 - Type string // 类型: node, sentinel, cluster - Pass string // 密码 - Db int // 数据库编号 (0-15),cluster 模式不支持 - - // Sentinel 模式专用 - MasterName string // Sentinel 主节点名称 - - // 连接池配置 - MaxIdle int // 最大闲置连接数 - MaxActive int // 最大活跃连接数,0 表示无限制 - IdleTimeout time.Duration // 闲置连接超时 - - // 超时配置 - ConnectTimeout time.Duration // 连接超时 - ReadTimeout time.Duration // 读超时 - WriteTimeout time.Duration // 写超时 - - // TLS 配置(可选) - Tls bool // 是否启用 TLS -} -``` - -### 各配置项说明 - -#### 1. Host(必填) - -**单节点模式:** -```yaml -Redis: - Host: user-redis-master.juwan.svc.cluster.local:6379 -``` - -**Sentinel 模式:** -```yaml -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - # 注意:这里填 Sentinel 地址(端口 26379),不是 Redis 地址 -``` - -**集群模式:** -```yaml -Redis: - # 可以填任意一个节点,客户端会自动发现其他节点 - - Host: redis-cluster-0:6379 - - Host: redis-cluster-1:6379 - - Host: redis-cluster-2:6379 -``` - -#### 2. Type(必填) - -| 值 | 说明 | -|----|------| -| `node` | 单节点模式 | -| `sentinel` | Sentinel 哨兵模式 | -| `cluster` | 集群模式 | - -#### 3. Pass(强烈推荐) - -**从环境变量读取(推荐):** -```yaml -Redis: - Pass: ${REDIS_PASSWORD} -``` - -**硬编码(不推荐):** -```yaml -Redis: - Pass: "your-password" # ❌ 不安全 -``` - -#### 4. Db(可选,默认 0) - -**适用模式:** 仅 `node` 和 `sentinel` 模式 - -```yaml -Redis: - Db: 0 # 数据库编号 0-15 -``` - -**注意:** -- ❌ Cluster 模式不支持多数据库 -- ✅ 单节点和 Sentinel 支持 0-15 - -#### 5. MaxIdle(可选,默认 8) - -```yaml -Redis: - MaxIdle: 8 # 连接池中最大闲置连接数 -``` - -**建议值:** -- 低并发:`8` -- 中并发:`16` -- 高并发:`32` 或 `CPU 核心数 * 2` - -#### 6. MaxActive(可选,默认 0) - -```yaml -Redis: - MaxActive: 0 # 0 表示无限制 - # MaxActive: 100 # 或设置一个上限 -``` - -**建议值:** -- 开发环境:`0`(无限制) -- 生产环境:`100-500`(根据实际负载) - -#### 7. IdleTimeout(可选,默认 300 秒) - -```yaml -Redis: - IdleTimeout: 300 # 秒 -``` - -**说明:** 闲置连接超过此时间会被关闭 - -#### 8. 超时配置(可选) - -```yaml -Redis: - ConnectTimeout: 5000 # 连接超时 5 秒 - ReadTimeout: 3000 # 读超时 3 秒 - WriteTimeout: 3000 # 写超时 3 秒 -``` - ---- - -## 💻 代码实现 - -### 完整项目结构 - -``` -app/users/rpc/ -├── etc/ -│ ├── pb.yaml # 开发环境配置 -│ └── pb-prod.yaml # 生产环境配置 -├── internal/ -│ ├── config/ -│ │ └── config.go # 配置结构定义 -│ ├── svc/ -│ │ └── serviceContext.go # 服务上下文(初始化 Redis) -│ ├── logic/ -│ │ ├── addUsersLogic.go -│ │ ├── getUsersByIdLogic.go -│ │ └── ... -│ └── server/ -│ └── usercenterServer.go -├── pb/ -│ ├── users.pb.go -│ └── users_grpc.pb.go -└── usercenter.go # 主程序入口 -``` - -### 1. 配置文件示例 - -**开发环境 `etc/pb.yaml`:** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# 开发环境使用单节点 -Redis: - Host: localhost:6379 - Type: node - Pass: dev_password - Db: 0 - -Etcd: - Hosts: - - localhost:2379 - Key: user.rpc - -Log: - Level: info - Mode: console -``` - -**生产环境 `etc/pb-prod.yaml`:** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# 生产环境使用 Sentinel -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - Pass: ${REDIS_PASSWORD} - MasterName: mymaster - MaxIdle: 16 - MaxActive: 100 - IdleTimeout: 300 - ConnectTimeout: 5000 - ReadTimeout: 3000 - WriteTimeout: 3000 - -Etcd: - Hosts: - - etcd-0.etcd.juwan.svc.cluster.local:2379 - - etcd-1.etcd.juwan.svc.cluster.local:2379 - - etcd-2.etcd.juwan.svc.cluster.local:2379 - Key: user.rpc - -Log: - Level: error - Mode: file - Path: /var/log/user-rpc - KeepDays: 7 -``` - -### 2. Config 定义 - -**`internal/config/config.go`** -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - - // Redis 配置 - Redis redis.RedisConf - - // 其他配置... - // DB postgres.Config - // Kafka kafka.Config -} -``` - -### 3. ServiceContext 初始化 - -**`internal/svc/serviceContext.go`** -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis - // 其他依赖... - // DB *gorm.DB -} - -func NewServiceContext(c config.Config) *ServiceContext { - // 初始化 Redis(支持所有模式:node, sentinel, cluster) - rdb := redis.MustNewRedis(c.Redis) - - return &ServiceContext{ - Config: c, - Redis: rdb, - } -} -``` - -### 4. 主程序入口 - -**`usercenter.go`** -```go -package main - -import ( - "flag" - "fmt" - - "juwan-backend/app/users/rpc/internal/config" - "juwan-backend/app/users/rpc/internal/server" - "juwan-backend/app/users/rpc/internal/svc" - "juwan-backend/app/users/rpc/pb" - - "github.com/zeromicro/go-zero/core/conf" - "github.com/zeromicro/go-zero/core/service" - "github.com/zeromicro/go-zero/zrpc" - "google.golang.org/grpc" - "google.golang.org/grpc/reflection" -) - -var configFile = flag.String("f", "etc/pb.yaml", "the config file") - -func main() { - flag.Parse() - - // 加载配置 - var c config.Config - conf.MustLoad(*configFile, &c) - - // 初始化服务上下文 - ctx := svc.NewServiceContext(c) - - // 创建 gRPC 服务 - s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { - pb.RegisterUsercenterServer(grpcServer, server.NewUsercenterServer(ctx)) - - if c.Mode == service.DevMode || c.Mode == service.TestMode { - reflection.Register(grpcServer) - } - }) - defer s.Stop() - - fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) - s.Start() -} -``` - ---- - -## 🔧 常用操作示例 - -### 1. 基本读写操作 - -```go -package logic - -import ( - "context" - "fmt" - "time" -) - -type UserLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -// Set 操作 -func (l *UserLogic) SetUser(userId int64, data string) error { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Set(key, data) -} - -// Setex 操作(带过期时间) -func (l *UserLogic) SetUserWithExpiry(userId int64, data string) error { - key := fmt.Sprintf("user:%d", userId) - // 缓存 1 小时 - return l.svcCtx.Redis.Setex(key, data, 3600) -} - -// Get 操作 -func (l *UserLogic) GetUser(userId int64) (string, error) { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Get(key) -} - -// Del 操作 -func (l *UserLogic) DeleteUser(userId int64) error { - key := fmt.Sprintf("user:%d", userId) - _, err := l.svcCtx.Redis.Del(key) - return err -} - -// Exists 检查 -func (l *UserLogic) UserExists(userId int64) (bool, error) { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Exists(key) -} -``` - -### 2. Hash 操作 - -```go -// HSet 操作 -func (l *UserLogic) SetUserField(userId int64, field, value string) error { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Hset(key, field, value) -} - -// HGet 操作 -func (l *UserLogic) GetUserField(userId int64, field string) (string, error) { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Hget(key, field) -} - -// HGetAll 操作 -func (l *UserLogic) GetAllUserFields(userId int64) (map[string]string, error) { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Hgetall(key) -} - -// HMSet 批量设置 -func (l *UserLogic) SetUserFields(userId int64, fields map[string]string) error { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Hmset(key, fields) -} -``` - -### 3. List 操作 - -```go -// LPush 操作 -func (l *UserLogic) AddMessage(userId int64, message string) error { - key := fmt.Sprintf("messages:%d", userId) - _, err := l.svcCtx.Redis.Lpush(key, message) - return err -} - -// LRange 操作 -func (l *UserLogic) GetMessages(userId int64, start, stop int) ([]string, error) { - key := fmt.Sprintf("messages:%d", userId) - return l.svcCtx.Redis.Lrange(key, start, stop) -} - -// LLen 操作 -func (l *UserLogic) GetMessageCount(userId int64) (int, error) { - key := fmt.Sprintf("messages:%d", userId) - return l.svcCtx.Redis.Llen(key) -} -``` - -### 4. Set 操作 - -```go -// SAdd 添加成员 -func (l *UserLogic) AddUserTag(userId int64, tag string) error { - key := fmt.Sprintf("user:tags:%d", userId) - _, err := l.svcCtx.Redis.Sadd(key, tag) - return err -} - -// SMembers 获取所有成员 -func (l *UserLogic) GetUserTags(userId int64) ([]string, error) { - key := fmt.Sprintf("user:tags:%d", userId) - return l.svcCtx.Redis.Smembers(key) -} - -// SIsMember 检查成员 -func (l *UserLogic) HasUserTag(userId int64, tag string) (bool, error) { - key := fmt.Sprintf("user:tags:%d", userId) - return l.svcCtx.Redis.Sismember(key, tag) -} -``` - -### 5. Sorted Set 操作 - -```go -// ZAdd 添加成员 -func (l *UserLogic) AddToLeaderboard(userId int64, score int64) error { - key := "leaderboard" - _, err := l.svcCtx.Redis.Zadd(key, score, fmt.Sprintf("%d", userId)) - return err -} - -// ZRevRange 获取排行榜(从高到低) -func (l *UserLogic) GetTopUsers(count int) ([]string, error) { - key := "leaderboard" - return l.svcCtx.Redis.Zrevrange(key, 0, int64(count-1)) -} - -// ZRank 获取排名 -func (l *UserLogic) GetUserRank(userId int64) (int64, error) { - key := "leaderboard" - return l.svcCtx.Redis.Zrank(key, fmt.Sprintf("%d", userId)) -} -``` - -### 6. 缓存模式实现 - -**Cache-Aside Pattern(推荐):** -```go -func (l *UserLogic) GetUserById(userId int64) (*User, error) { - cacheKey := fmt.Sprintf("user:%d", userId) - - // 1. 查缓存 - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - var user User - if err := json.Unmarshal([]byte(cached), &user); err == nil { - return &user, nil - } - } - - // 2. 查数据库 - user, err := l.getUserFromDB(userId) - if err != nil { - return nil, err - } - - // 3. 写缓存 - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - - return user, nil -} - -func (l *UserLogic) UpdateUser(user *User) error { - // 1. 更新数据库 - if err := l.updateUserInDB(user); err != nil { - return err - } - - // 2. 删除缓存(下次读取时会重新加载) - cacheKey := fmt.Sprintf("user:%d", user.Id) - l.svcCtx.Redis.Del(cacheKey) - - return nil -} -``` - -### 7. 分布式锁 - -```go -// 获取分布式锁 -func (l *UserLogic) AcquireLock(key string, expiry int) (bool, error) { - lockKey := fmt.Sprintf("lock:%s", key) - return l.svcCtx.Redis.Setnx(lockKey, "1") -} - -// 释放锁 -func (l *UserLogic) ReleaseLock(key string) error { - lockKey := fmt.Sprintf("lock:%s", key) - _, err := l.svcCtx.Redis.Del(lockKey) - return err -} - -// 使用示例 -func (l *UserLogic) ProcessWithLock(userId int64) error { - lockKey := fmt.Sprintf("user:%d", userId) - - // 获取锁 - acquired, err := l.AcquireLock(lockKey, 10) - if err != nil { - return err - } - if !acquired { - return fmt.Errorf("failed to acquire lock") - } - defer l.ReleaseLock(lockKey) - - // 执行业务逻辑 - // ... - - return nil -} -``` - -### 8. Pipeline 批量操作 - -```go -// 使用 go-redis 原生客户端进行 Pipeline -func (l *UserLogic) BatchSetUsers(users []*User) error { - // go-zero 的 Redis 包装了 go-redis,可以获取原生客户端 - rdb := l.svcCtx.Redis - - pipe := rdb.Pipelined(func(pip redis.Pipeliner) error { - for _, user := range users { - key := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - pip.Set(context.Background(), key, userJSON, time.Hour) - } - return nil - }) - - return pipe -} -``` - ---- - -## 🚀 高级特性 - -### 1. 缓存穿透防护(布隆过滤器) - -```go -import "github.com/zeromicro/go-zero/core/bloom" - -type UserLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - filter *bloom.Filter -} - -func (l *UserLogic) GetUserWithBloom(userId int64) (*User, error) { - // 1. 布隆过滤器检查 - if !l.filter.Exists([]byte(fmt.Sprintf("%d", userId))) { - return nil, fmt.Errorf("user not found") - } - - // 2. 查缓存 - cacheKey := fmt.Sprintf("user:%d", userId) - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - var user User - json.Unmarshal([]byte(cached), &user) - return &user, nil - } - - // 3. 查数据库 - user, err := l.getUserFromDB(userId) - if err != nil { - return nil, err - } - - // 4. 写缓存 - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - - return user, nil -} -``` - -### 2. 缓存击穿防护(Singleflight) - -```go -import "golang.org/x/sync/singleflight" - -type UserLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - sg singleflight.Group -} - -func (l *UserLogic) GetUserWithSingleflight(userId int64) (*User, error) { - cacheKey := fmt.Sprintf("user:%d", userId) - - // 使用 Singleflight 确保同一时刻只有一个请求查询 - v, err, _ := l.sg.Do(cacheKey, func() (interface{}, error) { - // 1. 查缓存 - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - var user User - json.Unmarshal([]byte(cached), &user) - return &user, nil - } - - // 2. 查数据库 - user, err := l.getUserFromDB(userId) - if err != nil { - return nil, err - } - - // 3. 写缓存 - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - - return user, nil - }) - - if err != nil { - return nil, err - } - - return v.(*User), nil -} -``` - -### 3. 缓存雪崩防护(随机过期时间) - -```go -import ( - "math/rand" - "time" -) - -func (l *UserLogic) SetCacheWithRandomExpiry(key string, value string, baseExpiry int) error { - // 在基础过期时间上增加随机值(±20%) - randomOffset := rand.Intn(baseExpiry / 5) - expiry := baseExpiry + randomOffset - (baseExpiry / 10) - - return l.svcCtx.Redis.Setex(key, value, expiry) -} - -// 使用示例 -func (l *UserLogic) CacheUser(user *User) error { - key := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - - // 基础过期时间 1 小时,实际会在 48-72 分钟之间随机 - return l.SetCacheWithRandomExpiry(key, string(userJSON), 3600) -} -``` - ---- - -## ⚡ 性能优化 - -### 1. 连接池配置优化 - -**根据并发量调整:** -```yaml -# 低并发(< 100 QPS) -Redis: - MaxIdle: 8 - MaxActive: 100 - -# 中并发(100-1000 QPS) -Redis: - MaxIdle: 16 - MaxActive: 500 - -# 高并发(> 1000 QPS) -Redis: - MaxIdle: 32 - MaxActive: 1000 -``` - -### 2. 超时配置优化 - -```yaml -Redis: - # 连接超时:通常设置较大值 - ConnectTimeout: 5000 # 5 秒 - - # 读写超时:设置较小值,快速失败 - ReadTimeout: 1000 # 1 秒 - WriteTimeout: 1000 # 1 秒 -``` - -### 3. Pipeline 批量操作 - -**避免循环调用:** -```go -// ❌ 不好:循环调用 -for _, user := range users { - key := fmt.Sprintf("user:%d", user.Id) - l.svcCtx.Redis.Set(key, user.Name) -} - -// ✅ 推荐:使用 Pipeline -pipe := l.svcCtx.Redis.Pipelined(func(pip redis.Pipeliner) error { - for _, user := range users { - key := fmt.Sprintf("user:%d", user.Id) - pip.Set(context.Background(), key, user.Name, 0) - } - return nil -}) -``` - -### 4. 合理的缓存过期时间 - -```go -const ( - CacheExpiryShort = 300 // 5 分钟 - 热点数据 - CacheExpiryMedium = 3600 // 1 小时 - 常规数据 - CacheExpiryLong = 86400 // 1 天 - 冷数据 -) - -func (l *UserLogic) SetUserCache(user *User, expiry int) error { - key := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - return l.svcCtx.Redis.Setex(key, string(userJSON), expiry) -} -``` - -### 5. Key 命名规范 - -```go -// 推荐的 Key 命名规范 -const ( - KeyPrefixUser = "user:" // user:123 - KeyPrefixSession = "session:" // session:abc123 - KeyPrefixCache = "cache:" // cache:user:list - KeyPrefixLock = "lock:" // lock:order:456 - KeyPrefixCounter = "counter:" // counter:page:views -) - -// 使用函数生成 Key -func UserCacheKey(userId int64) string { - return fmt.Sprintf("%s%d", KeyPrefixUser, userId) -} - -func SessionKey(sessionId string) string { - return fmt.Sprintf("%s%s", KeyPrefixSession, sessionId) -} -``` - ---- - -## 🔍 故障排查 - -### 1. 连接失败 - -**问题:** `dial tcp xxx:6379: i/o timeout` - -**排查步骤:** -```bash -# 1. 检查 Redis 服务 -kubectl get pods -n juwan | grep redis - -# 2. 检查 Service -kubectl get svc -n juwan | grep redis - -# 3. 测试网络连通性 -kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \ - nc -zv user-redis-master 6379 - -# 4. 查看应用日志 -kubectl logs -f user-rpc-xxx -n juwan -``` - -**解决方案:** -- 确认 Host 配置正确 -- 确认网络策略允许访问 -- 检查 Redis Pod 状态 - -### 2. 认证失败 - -**问题:** `NOAUTH Authentication required` - -**排查:** -```go -// 打印配置(调试用) -func main() { - var c config.Config - conf.MustLoad(*configFile, &c) - - // 检查密码是否正确加载 - fmt.Printf("Redis Config: Host=%s, Pass=%s\n", c.Redis.Host, c.Redis.Pass) -} -``` - -**解决方案:** -- 确认环境变量 `REDIS_PASSWORD` 已设置 -- 确认 Secret 正确挂载 -- 检查密码是否正确 - -### 3. 性能问题 - -**慢查询检测:** -```go -import "time" - -func (l *UserLogic) GetUserWithMetrics(userId int64) (*User, error) { - start := time.Now() - defer func() { - duration := time.Since(start) - if duration > 100*time.Millisecond { - l.Logger.Warnf("Slow Redis query: %v", duration) - } - }() - - // 执行查询 - key := fmt.Sprintf("user:%d", userId) - cached, err := l.svcCtx.Redis.Get(key) - // ... -} -``` - -**常见原因:** -- 连接池耗尽 → 增大 MaxActive -- 大 Value 传输 → 拆分或压缩数据 -- 网络延迟 → 检查网络质量 - -### 4. 内存泄漏 - -**检查连接是否正确关闭:** -```go -// go-zero 的 Redis 客户端会自动管理连接 -// 但如果使用原生 go-redis 客户端,需要手动关闭 - -// ❌ 错误示例 -func bad() { - rdb := redis.NewClient(&redis.Options{...}) - // 使用完后没有关闭 -} - -// ✅ 正确示例 -func good() { - rdb := redis.NewClient(&redis.Options{...}) - defer rdb.Close() - // ... -} -``` - ---- - -## 📖 最佳实践 - -### 1. 环境变量管理 - -**Kubernetes Deployment:** -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: user-rpc - namespace: juwan -spec: - template: - spec: - containers: - - name: user-rpc - image: user-rpc:v1 - env: - # 从 Secret 读取 Redis 密码 - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: user-redis - key: password - - # 从 ConfigMap 读取其他配置 - - name: REDIS_HOST - valueFrom: - configMapKeyRef: - name: user-rpc-config - key: redis.host -``` - -### 2. 配置分离 - -**开发、测试、生产环境分离:** -```bash -# 开发环境 -go run usercenter.go -f etc/pb-dev.yaml - -# 测试环境 -go run usercenter.go -f etc/pb-test.yaml - -# 生产环境 -./usercenter -f etc/pb-prod.yaml -``` - -### 3. 监控指标 - -```go -import ( - "github.com/zeromicro/go-zero/core/metric" - "github.com/zeromicro/go-zero/core/prometheus" -) - -var ( - redisCacheHit = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: "user_rpc", - Subsystem: "redis", - Name: "cache_hit_total", - Help: "redis cache hit count", - Labels: []string{"key"}, - }) - - redisCacheMiss = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: "user_rpc", - Subsystem: "redis", - Name: "cache_miss_total", - Help: "redis cache miss count", - Labels: []string{"key"}, - }) -) - -func (l *UserLogic) GetUserWithMetrics(userId int64) (*User, error) { - cacheKey := fmt.Sprintf("user:%d", userId) - - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - redisCacheHit.Inc("user") - var user User - json.Unmarshal([]byte(cached), &user) - return &user, nil - } - - redisCacheMiss.Inc("user") - // 查询数据库... -} -``` - -### 4. 错误处理 - -```go -func (l *UserLogic) GetUser(userId int64) (*User, error) { - cacheKey := fmt.Sprintf("user:%d", userId) - - // 缓存查询失败不应该中断流程 - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - var user User - if json.Unmarshal([]byte(cached), &user) == nil { - return &user, nil - } - } - - // 缓存失败,降级查询数据库 - user, err := l.getUserFromDB(userId) - if err != nil { - return nil, err - } - - // 尝试回写缓存,失败不影响返回结果 - go func() { - userJSON, _ := json.Marshal(user) - if err := l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600); err != nil { - l.Logger.Errorf("Failed to set cache: %v", err) - } - }() - - return user, nil -} -``` - -### 5. 缓存更新策略 - -**Write-Through(同步更新):** -```go -func (l *UserLogic) UpdateUser(user *User) error { - // 1. 更新数据库 - if err := l.updateUserInDB(user); err != nil { - return err - } - - // 2. 同步更新缓存 - cacheKey := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - if err := l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600); err != nil { - l.Logger.Errorf("Failed to update cache: %v", err) - } - - return nil -} -``` - -**Write-Behind(异步更新):** -```go -func (l *UserLogic) UpdateUserAsync(user *User) error { - // 1. 立即更新缓存 - cacheKey := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - - // 2. 异步更新数据库 - go func() { - if err := l.updateUserInDB(user); err != nil { - l.Logger.Errorf("Failed to update DB: %v", err) - // 回滚缓存 - l.svcCtx.Redis.Del(cacheKey) - } - }() - - return nil -} -``` - ---- - -## 📚 参考资源 - -### 官方文档 -- [go-zero 官方文档](https://go-zero.dev/) -- [go-zero Redis 文档](https://go-zero.dev/docs/tutorials/redis) -- [go-redis 文档](https://redis.uptrace.dev/) - -### 示例代码 -- [go-zero Examples](https://github.com/zeromicro/go-zero/tree/master/example) -- [go-zero Book Store](https://github.com/zeromicro/go-zero-book-store) - -### 相关工具 -- [RedisInsight](https://redis.com/redis-enterprise/redis-insight/) - Redis 管理工具 -- [redis-cli](https://redis.io/docs/manual/cli/) - Redis 命令行工具 - ---- - -## 📝 总结 - -### 快速开始检查清单 - -- [ ] 1. 在 `config.go` 中定义 `Redis redis.RedisConf` -- [ ] 2. 在配置文件中添加 Redis 配置 -- [ ] 3. 在 `ServiceContext` 中初始化 `redis.MustNewRedis(c.Redis)` -- [ ] 4. 在 Kubernetes中配置环境变量 `REDIS_PASSWORD` -- [ ] 5. 在 logic 中使用 `l.svcCtx.Redis` -- [ ] 6. 测试连接是否正常 - -### 生产环境推荐配置 - -```yaml -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - Pass: ${REDIS_PASSWORD} - MasterName: mymaster - MaxIdle: 16 - MaxActive: 100 - ConnectTimeout: 5000 - ReadTimeout: 3000 - WriteTimeout: 3000 -``` - -### 关键点提醒 - -1. ✅ **生产环境必须使用 Sentinel 模式** -2. ✅ **密码通过环境变量传递,不要硬编码** -3. ✅ **合理设置过期时间,防止缓存雪崩** -4. ✅ **使用 Pipeline 优化批量操作** -5. ✅ **实现缓存降级策略,Redis 故障不影响主流程** - ---- - -**文档版本:** 1.0 -**创建日期:** 2026年2月22日 -**维护者:** Backend Team -**下次审查:** 2026年3月22日 +# Go-Zero 框架 Redis 配置完全指南 + +**框架版本:** go-zero v1.5+ +**Redis 版本:** 7.0.12 +**部署环境:** Kubernetes (juwan namespace) +**文档日期:** 2026年2月22日 + +--- + +## 📋 目录 + +1. [配置概览](#配置概览) +2. [单节点模式](#单节点模式) +3. [Sentinel 哨兵模式](#sentinel-哨兵模式) +4. [集群模式](#集群模式) +5. [配置项详解](#配置项详解) +6. [代码实现](#代码实现) +7. [常用操作示例](#常用操作示例) +8. [高级特性](#高级特性) +9. [性能优化](#性能优化) +10. [故障排查](#故障排查) +11. [最佳实践](#最佳实践) + +--- + +## 🎯 配置概览 + +### Go-Zero Redis 支持的模式 + +| 模式 | Type 值 | 用途 | 高可用 | 推荐度 | +|-----|---------|------|--------|--------| +| **单节点** | `node` | 开发/测试 | ❌ | ⭐⭐ | +| **Sentinel** | `sentinel` | 生产环境 | ✅ | ⭐⭐⭐⭐⭐ | +| **集群** | `cluster` | 大规模分片 | ✅ | ⭐⭐⭐⭐ | + +### 配置文件位置 + +``` +app/users/rpc/ +├── etc/ +│ └── pb.yaml ← Redis 配置写在这里 +├── internal/ +│ ├── config/ +│ │ └── config.go ← 定义配置结构 +│ └── svc/ +│ └── serviceContext.go ← 初始化 Redis 客户端 +└── pb.go +``` + +--- + +## 🔵 单节点模式 + +### 适用场景 + +- ✅ 开发环境 +- ✅ 测试环境 +- ✅ POC 演示 +- ❌ 生产环境(无高可用) + +### 配置文件 + +**`app/users/rpc/etc/pb.yaml`** +```yaml +Name: user.rpc +ListenOn: 0.0.0.0:9001 + +# Redis 单节点配置 +Redis: + Host: user-redis-master.juwan.svc.cluster.local:6379 + Type: node + Pass: ${REDIS_PASSWORD} # 从环境变量读取 + # Db: 0 # 可选,默认 0 + # MaxIdle: 8 # 可选,连接池最大闲置连接数 + # MaxActive: 0 # 可选,连接池最大活跃连接数,0 表示无限制 + +Etcd: + Hosts: + - etcd-service.juwan.svc.cluster.local:2379 + Key: user.rpc +``` + +### Config 结构 + +**`app/users/rpc/internal/config/config.go`** +```go +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + Redis redis.RedisConf // Redis 配置 +} +``` + +### ServiceContext 初始化 + +**`app/users/rpc/internal/svc/serviceContext.go`** +```go +package svc + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "juwan-backend/app/users/rpc/internal/config" +) + +type ServiceContext struct { + Config config.Config + Redis *redis.Redis +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + Redis: redis.MustNewRedis(c.Redis), // 初始化 Redis + } +} +``` + +### 使用示例 + +**`app/users/rpc/internal/logic/getUsersByIdLogic.go`** +```go +package logic + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "juwan-backend/app/users/rpc/internal/svc" + "juwan-backend/app/users/rpc/pb" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUsersByIdLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGetUsersByIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUsersByIdLogic { + return &GetUsersByIdLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +func (l *GetUsersByIdLogic) GetUsersById(in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { + // 缓存 key + cacheKey := fmt.Sprintf("user:%d", in.Id) + + // 1. 尝试从缓存获取 + cached, err := l.svcCtx.Redis.Get(cacheKey) + if err == nil && cached != "" { + // 缓存命中 + var user pb.User + if err := json.Unmarshal([]byte(cached), &user); err == nil { + l.Logger.Infof("Cache hit for user:%d", in.Id) + return &pb.GetUsersByIdResp{User: &user}, nil + } + } + + // 2. 缓存未命中,从数据库查询 + l.Logger.Infof("Cache miss for user:%d, querying DB", in.Id) + user := l.fetchUserFromDB(in.Id) + if user == nil { + return nil, fmt.Errorf("user not found") + } + + // 3. 写入缓存(1小时过期) + userJSON, _ := json.Marshal(user) + err = l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) + if err != nil { + l.Logger.Errorf("Failed to set cache: %v", err) + } + + return &pb.GetUsersByIdResp{User: user}, nil +} + +func (l *GetUsersByIdLogic) fetchUserFromDB(id int64) *pb.User { + // 实际从数据库查询 + // ... + return &pb.User{Id: id, Name: "John Doe"} +} +``` + +--- + +## 🟡 Sentinel 哨兵模式 + +### 适用场景 + +- ✅✅✅ **生产环境强烈推荐** +- ✅ 自动故障转移 +- ✅ 高可用架构 +- ✅ 主从自动切换 + +### 配置文件 + +**`app/users/rpc/etc/pb.yaml`** +```yaml +Name: user.rpc +ListenOn: 0.0.0.0:9001 + +# Redis Sentinel 配置(推荐生产环境使用) +Redis: + - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Type: sentinel + Pass: ${REDIS_PASSWORD} + +# 或者使用完整配置 +# Redis: +# Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 +# Type: sentinel +# Pass: ${REDIS_PASSWORD} +# # Sentinel 特有配置 +# MasterName: mymaster # Sentinel 主节点名称,默认 mymaster + +Etcd: + Hosts: + - etcd-service.juwan.svc.cluster.local:2379 + Key: user.rpc +``` + +### Config 结构(同单节点) + +**`app/users/rpc/internal/config/config.go`** +```go +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + Redis redis.RedisConf // 支持所有模式 +} +``` + +### ServiceContext 初始化(同单节点) + +**`app/users/rpc/internal/svc/serviceContext.go`** +```go +package svc + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "juwan-backend/app/users/rpc/internal/config" +) + +type ServiceContext struct { + Config config.Config + Redis *redis.Redis +} + +func NewServiceContext(c config.Config) *ServiceContext { + // go-zero 会根据 Type 自动选择连接模式 + return &ServiceContext{ + Config: c, + Redis: redis.MustNewRedis(c.Redis), + } +} +``` + +### Sentinel 配置详解 + +**完整配置选项:** +```yaml +Redis: + Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Type: sentinel + Pass: ${REDIS_PASSWORD} + + # Sentinel 特有配置 + MasterName: mymaster # Sentinel 监控的主节点名称 + + # 连接池配置(可选) + MaxIdle: 8 # 最大闲置连接数 + MaxActive: 0 # 最大活跃连接数,0 表示无限制 + IdleTimeout: 300 # 闲置连接超时时间(秒) + + # 超时配置(可选) + ConnectTimeout: 5000 # 连接超时(毫秒) + ReadTimeout: 3000 # 读超时(毫秒) + WriteTimeout: 3000 # 写超时(毫秒) +``` + +### 优势说明 + +```go +// Sentinel 模式的自动故障处理流程: + +// 1. 应用连接到 Sentinel +app → Sentinel Service (26379) + +// 2. Sentinel 返回当前主节点地址 +Sentinel → app: "主节点在 10.244.1.10:6379" + +// 3. 应用连接到主节点进行读写 +app → Redis Master (10.244.1.10:6379) + +// 4. 主节点故障,Sentinel 检测到 +Redis Master (✗ 宕机) +Sentinel → 检测 → 投票 → 提升新主节点 + +// 5. 应用下次请求时自动连接到新主节点 +app → Sentinel → "新主节点在 10.244.2.20:6379" +app → New Redis Master (10.244.2.20:6379) + +// 整个过程应用无需重启,自动完成切换! +``` + +--- + +## 🔴 集群模式 + +### 适用场景 + +- ✅ 大规模数据(需要分片) +- ✅ 超高并发 +- ✅ 数据量超过单机内存 +- ⚠️ 配置和运维复杂度高 + +### 配置文件 + +**`app/users/rpc/etc/pb.yaml`** +```yaml +Name: user.rpc +ListenOn: 0.0.0.0:9001 + +# Redis Cluster 配置 +Redis: + - Host: redis-cluster-0.redis-cluster.juwan.svc.cluster.local:6379 + - Host: redis-cluster-1.redis-cluster.juwan.svc.cluster.local:6379 + - Host: redis-cluster-2.redis-cluster.juwan.svc.cluster.local:6379 + Type: cluster + Pass: ${REDIS_PASSWORD} + +Etcd: + Hosts: + - etcd-service.juwan.svc.cluster.local:2379 + Key: user.rpc +``` + +### Config 结构 + +**`app/users/rpc/internal/config/config.go`** +```go +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + Redis redis.RedisConf +} +``` + +### 集群模式特点 + +**数据分片:** +``` +应用请求 + ↓ +根据 key 计算 hash slot (0-16383) + ↓ +路由到对应的分片节点 + ↓ +┌─────────┬─────────┬─────────┐ +│ Shard 1 │ Shard 2 │ Shard 3 │ +│ 0-5460 │5461-10922│10923-16383│ +└─────────┴─────────┴─────────┘ +``` + +**注意事项:** +- ❌ 不支持多 key 操作(如 MGET, MSET)跨分片 +- ❌ 不支持事务(MULTI/EXEC)跨分片 +- ✅ 单 key 操作完全正常 +- ✅ 支持 hash tag 控制 key 分布 + +--- + +## 📚 配置项详解 + +### redis.RedisConf 完整配置 + +**结构定义:** +```go +type RedisConf struct { + Host string // Redis 地址 + Type string // 类型: node, sentinel, cluster + Pass string // 密码 + Db int // 数据库编号 (0-15),cluster 模式不支持 + + // Sentinel 模式专用 + MasterName string // Sentinel 主节点名称 + + // 连接池配置 + MaxIdle int // 最大闲置连接数 + MaxActive int // 最大活跃连接数,0 表示无限制 + IdleTimeout time.Duration // 闲置连接超时 + + // 超时配置 + ConnectTimeout time.Duration // 连接超时 + ReadTimeout time.Duration // 读超时 + WriteTimeout time.Duration // 写超时 + + // TLS 配置(可选) + Tls bool // 是否启用 TLS +} +``` + +### 各配置项说明 + +#### 1. Host(必填) + +**单节点模式:** +```yaml +Redis: + Host: user-redis-master.juwan.svc.cluster.local:6379 +``` + +**Sentinel 模式:** +```yaml +Redis: + Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + # 注意:这里填 Sentinel 地址(端口 26379),不是 Redis 地址 +``` + +**集群模式:** +```yaml +Redis: + # 可以填任意一个节点,客户端会自动发现其他节点 + - Host: redis-cluster-0:6379 + - Host: redis-cluster-1:6379 + - Host: redis-cluster-2:6379 +``` + +#### 2. Type(必填) + +| 值 | 说明 | +|----|------| +| `node` | 单节点模式 | +| `sentinel` | Sentinel 哨兵模式 | +| `cluster` | 集群模式 | + +#### 3. Pass(强烈推荐) + +**从环境变量读取(推荐):** +```yaml +Redis: + Pass: ${REDIS_PASSWORD} +``` + +**硬编码(不推荐):** +```yaml +Redis: + Pass: "your-password" # ❌ 不安全 +``` + +#### 4. Db(可选,默认 0) + +**适用模式:** 仅 `node` 和 `sentinel` 模式 + +```yaml +Redis: + Db: 0 # 数据库编号 0-15 +``` + +**注意:** +- ❌ Cluster 模式不支持多数据库 +- ✅ 单节点和 Sentinel 支持 0-15 + +#### 5. MaxIdle(可选,默认 8) + +```yaml +Redis: + MaxIdle: 8 # 连接池中最大闲置连接数 +``` + +**建议值:** +- 低并发:`8` +- 中并发:`16` +- 高并发:`32` 或 `CPU 核心数 * 2` + +#### 6. MaxActive(可选,默认 0) + +```yaml +Redis: + MaxActive: 0 # 0 表示无限制 + # MaxActive: 100 # 或设置一个上限 +``` + +**建议值:** +- 开发环境:`0`(无限制) +- 生产环境:`100-500`(根据实际负载) + +#### 7. IdleTimeout(可选,默认 300 秒) + +```yaml +Redis: + IdleTimeout: 300 # 秒 +``` + +**说明:** 闲置连接超过此时间会被关闭 + +#### 8. 超时配置(可选) + +```yaml +Redis: + ConnectTimeout: 5000 # 连接超时 5 秒 + ReadTimeout: 3000 # 读超时 3 秒 + WriteTimeout: 3000 # 写超时 3 秒 +``` + +--- + +## 💻 代码实现 + +### 完整项目结构 + +``` +app/users/rpc/ +├── etc/ +│ ├── pb.yaml # 开发环境配置 +│ └── pb-prod.yaml # 生产环境配置 +├── internal/ +│ ├── config/ +│ │ └── config.go # 配置结构定义 +│ ├── svc/ +│ │ └── serviceContext.go # 服务上下文(初始化 Redis) +│ ├── logic/ +│ │ ├── addUsersLogic.go +│ │ ├── getUsersByIdLogic.go +│ │ └── ... +│ └── server/ +│ └── usercenterServer.go +├── pb/ +│ ├── users.pb.go +│ └── users_grpc.pb.go +└── usercenter.go # 主程序入口 +``` + +### 1. 配置文件示例 + +**开发环境 `etc/pb.yaml`:** +```yaml +Name: user.rpc +ListenOn: 0.0.0.0:9001 + +# 开发环境使用单节点 +Redis: + Host: localhost:6379 + Type: node + Pass: dev_password + Db: 0 + +Etcd: + Hosts: + - localhost:2379 + Key: user.rpc + +Log: + Level: info + Mode: console +``` + +**生产环境 `etc/pb-prod.yaml`:** +```yaml +Name: user.rpc +ListenOn: 0.0.0.0:9001 + +# 生产环境使用 Sentinel +Redis: + Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Type: sentinel + Pass: ${REDIS_PASSWORD} + MasterName: mymaster + MaxIdle: 16 + MaxActive: 100 + IdleTimeout: 300 + ConnectTimeout: 5000 + ReadTimeout: 3000 + WriteTimeout: 3000 + +Etcd: + Hosts: + - etcd-0.etcd.juwan.svc.cluster.local:2379 + - etcd-1.etcd.juwan.svc.cluster.local:2379 + - etcd-2.etcd.juwan.svc.cluster.local:2379 + Key: user.rpc + +Log: + Level: error + Mode: file + Path: /var/log/user-rpc + KeepDays: 7 +``` + +### 2. Config 定义 + +**`internal/config/config.go`** +```go +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + + // Redis 配置 + Redis redis.RedisConf + + // 其他配置... + // DB postgres.Config + // Kafka kafka.Config +} +``` + +### 3. ServiceContext 初始化 + +**`internal/svc/serviceContext.go`** +```go +package svc + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "juwan-backend/app/users/rpc/internal/config" +) + +type ServiceContext struct { + Config config.Config + Redis *redis.Redis + // 其他依赖... + // DB *gorm.DB +} + +func NewServiceContext(c config.Config) *ServiceContext { + // 初始化 Redis(支持所有模式:node, sentinel, cluster) + rdb := redis.MustNewRedis(c.Redis) + + return &ServiceContext{ + Config: c, + Redis: rdb, + } +} +``` + +### 4. 主程序入口 + +**`usercenter.go`** +```go +package main + +import ( + "flag" + "fmt" + + "juwan-backend/app/users/rpc/internal/config" + "juwan-backend/app/users/rpc/internal/server" + "juwan-backend/app/users/rpc/internal/svc" + "juwan-backend/app/users/rpc/pb" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/core/service" + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var configFile = flag.String("f", "etc/pb.yaml", "the config file") + +func main() { + flag.Parse() + + // 加载配置 + var c config.Config + conf.MustLoad(*configFile, &c) + + // 初始化服务上下文 + ctx := svc.NewServiceContext(c) + + // 创建 gRPC 服务 + s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { + pb.RegisterUsercenterServer(grpcServer, server.NewUsercenterServer(ctx)) + + if c.Mode == service.DevMode || c.Mode == service.TestMode { + reflection.Register(grpcServer) + } + }) + defer s.Stop() + + fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) + s.Start() +} +``` + +--- + +## 🔧 常用操作示例 + +### 1. 基本读写操作 + +```go +package logic + +import ( + "context" + "fmt" + "time" +) + +type UserLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +// Set 操作 +func (l *UserLogic) SetUser(userId int64, data string) error { + key := fmt.Sprintf("user:%d", userId) + return l.svcCtx.Redis.Set(key, data) +} + +// Setex 操作(带过期时间) +func (l *UserLogic) SetUserWithExpiry(userId int64, data string) error { + key := fmt.Sprintf("user:%d", userId) + // 缓存 1 小时 + return l.svcCtx.Redis.Setex(key, data, 3600) +} + +// Get 操作 +func (l *UserLogic) GetUser(userId int64) (string, error) { + key := fmt.Sprintf("user:%d", userId) + return l.svcCtx.Redis.Get(key) +} + +// Del 操作 +func (l *UserLogic) DeleteUser(userId int64) error { + key := fmt.Sprintf("user:%d", userId) + _, err := l.svcCtx.Redis.Del(key) + return err +} + +// Exists 检查 +func (l *UserLogic) UserExists(userId int64) (bool, error) { + key := fmt.Sprintf("user:%d", userId) + return l.svcCtx.Redis.Exists(key) +} +``` + +### 2. Hash 操作 + +```go +// HSet 操作 +func (l *UserLogic) SetUserField(userId int64, field, value string) error { + key := fmt.Sprintf("user:%d", userId) + return l.svcCtx.Redis.Hset(key, field, value) +} + +// HGet 操作 +func (l *UserLogic) GetUserField(userId int64, field string) (string, error) { + key := fmt.Sprintf("user:%d", userId) + return l.svcCtx.Redis.Hget(key, field) +} + +// HGetAll 操作 +func (l *UserLogic) GetAllUserFields(userId int64) (map[string]string, error) { + key := fmt.Sprintf("user:%d", userId) + return l.svcCtx.Redis.Hgetall(key) +} + +// HMSet 批量设置 +func (l *UserLogic) SetUserFields(userId int64, fields map[string]string) error { + key := fmt.Sprintf("user:%d", userId) + return l.svcCtx.Redis.Hmset(key, fields) +} +``` + +### 3. List 操作 + +```go +// LPush 操作 +func (l *UserLogic) AddMessage(userId int64, message string) error { + key := fmt.Sprintf("messages:%d", userId) + _, err := l.svcCtx.Redis.Lpush(key, message) + return err +} + +// LRange 操作 +func (l *UserLogic) GetMessages(userId int64, start, stop int) ([]string, error) { + key := fmt.Sprintf("messages:%d", userId) + return l.svcCtx.Redis.Lrange(key, start, stop) +} + +// LLen 操作 +func (l *UserLogic) GetMessageCount(userId int64) (int, error) { + key := fmt.Sprintf("messages:%d", userId) + return l.svcCtx.Redis.Llen(key) +} +``` + +### 4. Set 操作 + +```go +// SAdd 添加成员 +func (l *UserLogic) AddUserTag(userId int64, tag string) error { + key := fmt.Sprintf("user:tags:%d", userId) + _, err := l.svcCtx.Redis.Sadd(key, tag) + return err +} + +// SMembers 获取所有成员 +func (l *UserLogic) GetUserTags(userId int64) ([]string, error) { + key := fmt.Sprintf("user:tags:%d", userId) + return l.svcCtx.Redis.Smembers(key) +} + +// SIsMember 检查成员 +func (l *UserLogic) HasUserTag(userId int64, tag string) (bool, error) { + key := fmt.Sprintf("user:tags:%d", userId) + return l.svcCtx.Redis.Sismember(key, tag) +} +``` + +### 5. Sorted Set 操作 + +```go +// ZAdd 添加成员 +func (l *UserLogic) AddToLeaderboard(userId int64, score int64) error { + key := "leaderboard" + _, err := l.svcCtx.Redis.Zadd(key, score, fmt.Sprintf("%d", userId)) + return err +} + +// ZRevRange 获取排行榜(从高到低) +func (l *UserLogic) GetTopUsers(count int) ([]string, error) { + key := "leaderboard" + return l.svcCtx.Redis.Zrevrange(key, 0, int64(count-1)) +} + +// ZRank 获取排名 +func (l *UserLogic) GetUserRank(userId int64) (int64, error) { + key := "leaderboard" + return l.svcCtx.Redis.Zrank(key, fmt.Sprintf("%d", userId)) +} +``` + +### 6. 缓存模式实现 + +**Cache-Aside Pattern(推荐):** +```go +func (l *UserLogic) GetUserById(userId int64) (*User, error) { + cacheKey := fmt.Sprintf("user:%d", userId) + + // 1. 查缓存 + cached, err := l.svcCtx.Redis.Get(cacheKey) + if err == nil && cached != "" { + var user User + if err := json.Unmarshal([]byte(cached), &user); err == nil { + return &user, nil + } + } + + // 2. 查数据库 + user, err := l.getUserFromDB(userId) + if err != nil { + return nil, err + } + + // 3. 写缓存 + userJSON, _ := json.Marshal(user) + l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) + + return user, nil +} + +func (l *UserLogic) UpdateUser(user *User) error { + // 1. 更新数据库 + if err := l.updateUserInDB(user); err != nil { + return err + } + + // 2. 删除缓存(下次读取时会重新加载) + cacheKey := fmt.Sprintf("user:%d", user.Id) + l.svcCtx.Redis.Del(cacheKey) + + return nil +} +``` + +### 7. 分布式锁 + +```go +// 获取分布式锁 +func (l *UserLogic) AcquireLock(key string, expiry int) (bool, error) { + lockKey := fmt.Sprintf("lock:%s", key) + return l.svcCtx.Redis.Setnx(lockKey, "1") +} + +// 释放锁 +func (l *UserLogic) ReleaseLock(key string) error { + lockKey := fmt.Sprintf("lock:%s", key) + _, err := l.svcCtx.Redis.Del(lockKey) + return err +} + +// 使用示例 +func (l *UserLogic) ProcessWithLock(userId int64) error { + lockKey := fmt.Sprintf("user:%d", userId) + + // 获取锁 + acquired, err := l.AcquireLock(lockKey, 10) + if err != nil { + return err + } + if !acquired { + return fmt.Errorf("failed to acquire lock") + } + defer l.ReleaseLock(lockKey) + + // 执行业务逻辑 + // ... + + return nil +} +``` + +### 8. Pipeline 批量操作 + +```go +// 使用 go-redis 原生客户端进行 Pipeline +func (l *UserLogic) BatchSetUsers(users []*User) error { + // go-zero 的 Redis 包装了 go-redis,可以获取原生客户端 + rdb := l.svcCtx.Redis + + pipe := rdb.Pipelined(func(pip redis.Pipeliner) error { + for _, user := range users { + key := fmt.Sprintf("user:%d", user.Id) + userJSON, _ := json.Marshal(user) + pip.Set(context.Background(), key, userJSON, time.Hour) + } + return nil + }) + + return pipe +} +``` + +--- + +## 🚀 高级特性 + +### 1. 缓存穿透防护(布隆过滤器) + +```go +import "github.com/zeromicro/go-zero/core/bloom" + +type UserLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + filter *bloom.Filter +} + +func (l *UserLogic) GetUserWithBloom(userId int64) (*User, error) { + // 1. 布隆过滤器检查 + if !l.filter.Exists([]byte(fmt.Sprintf("%d", userId))) { + return nil, fmt.Errorf("user not found") + } + + // 2. 查缓存 + cacheKey := fmt.Sprintf("user:%d", userId) + cached, err := l.svcCtx.Redis.Get(cacheKey) + if err == nil && cached != "" { + var user User + json.Unmarshal([]byte(cached), &user) + return &user, nil + } + + // 3. 查数据库 + user, err := l.getUserFromDB(userId) + if err != nil { + return nil, err + } + + // 4. 写缓存 + userJSON, _ := json.Marshal(user) + l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) + + return user, nil +} +``` + +### 2. 缓存击穿防护(Singleflight) + +```go +import "golang.org/x/sync/singleflight" + +type UserLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + sg singleflight.Group +} + +func (l *UserLogic) GetUserWithSingleflight(userId int64) (*User, error) { + cacheKey := fmt.Sprintf("user:%d", userId) + + // 使用 Singleflight 确保同一时刻只有一个请求查询 + v, err, _ := l.sg.Do(cacheKey, func() (interface{}, error) { + // 1. 查缓存 + cached, err := l.svcCtx.Redis.Get(cacheKey) + if err == nil && cached != "" { + var user User + json.Unmarshal([]byte(cached), &user) + return &user, nil + } + + // 2. 查数据库 + user, err := l.getUserFromDB(userId) + if err != nil { + return nil, err + } + + // 3. 写缓存 + userJSON, _ := json.Marshal(user) + l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) + + return user, nil + }) + + if err != nil { + return nil, err + } + + return v.(*User), nil +} +``` + +### 3. 缓存雪崩防护(随机过期时间) + +```go +import ( + "math/rand" + "time" +) + +func (l *UserLogic) SetCacheWithRandomExpiry(key string, value string, baseExpiry int) error { + // 在基础过期时间上增加随机值(±20%) + randomOffset := rand.Intn(baseExpiry / 5) + expiry := baseExpiry + randomOffset - (baseExpiry / 10) + + return l.svcCtx.Redis.Setex(key, value, expiry) +} + +// 使用示例 +func (l *UserLogic) CacheUser(user *User) error { + key := fmt.Sprintf("user:%d", user.Id) + userJSON, _ := json.Marshal(user) + + // 基础过期时间 1 小时,实际会在 48-72 分钟之间随机 + return l.SetCacheWithRandomExpiry(key, string(userJSON), 3600) +} +``` + +--- + +## ⚡ 性能优化 + +### 1. 连接池配置优化 + +**根据并发量调整:** +```yaml +# 低并发(< 100 QPS) +Redis: + MaxIdle: 8 + MaxActive: 100 + +# 中并发(100-1000 QPS) +Redis: + MaxIdle: 16 + MaxActive: 500 + +# 高并发(> 1000 QPS) +Redis: + MaxIdle: 32 + MaxActive: 1000 +``` + +### 2. 超时配置优化 + +```yaml +Redis: + # 连接超时:通常设置较大值 + ConnectTimeout: 5000 # 5 秒 + + # 读写超时:设置较小值,快速失败 + ReadTimeout: 1000 # 1 秒 + WriteTimeout: 1000 # 1 秒 +``` + +### 3. Pipeline 批量操作 + +**避免循环调用:** +```go +// ❌ 不好:循环调用 +for _, user := range users { + key := fmt.Sprintf("user:%d", user.Id) + l.svcCtx.Redis.Set(key, user.Name) +} + +// ✅ 推荐:使用 Pipeline +pipe := l.svcCtx.Redis.Pipelined(func(pip redis.Pipeliner) error { + for _, user := range users { + key := fmt.Sprintf("user:%d", user.Id) + pip.Set(context.Background(), key, user.Name, 0) + } + return nil +}) +``` + +### 4. 合理的缓存过期时间 + +```go +const ( + CacheExpiryShort = 300 // 5 分钟 - 热点数据 + CacheExpiryMedium = 3600 // 1 小时 - 常规数据 + CacheExpiryLong = 86400 // 1 天 - 冷数据 +) + +func (l *UserLogic) SetUserCache(user *User, expiry int) error { + key := fmt.Sprintf("user:%d", user.Id) + userJSON, _ := json.Marshal(user) + return l.svcCtx.Redis.Setex(key, string(userJSON), expiry) +} +``` + +### 5. Key 命名规范 + +```go +// 推荐的 Key 命名规范 +const ( + KeyPrefixUser = "user:" // user:123 + KeyPrefixSession = "session:" // session:abc123 + KeyPrefixCache = "cache:" // cache:user:list + KeyPrefixLock = "lock:" // lock:order:456 + KeyPrefixCounter = "counter:" // counter:page:views +) + +// 使用函数生成 Key +func UserCacheKey(userId int64) string { + return fmt.Sprintf("%s%d", KeyPrefixUser, userId) +} + +func SessionKey(sessionId string) string { + return fmt.Sprintf("%s%s", KeyPrefixSession, sessionId) +} +``` + +--- + +## 🔍 故障排查 + +### 1. 连接失败 + +**问题:** `dial tcp xxx:6379: i/o timeout` + +**排查步骤:** +```bash +# 1. 检查 Redis 服务 +kubectl get pods -n juwan | grep redis + +# 2. 检查 Service +kubectl get svc -n juwan | grep redis + +# 3. 测试网络连通性 +kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \ + nc -zv user-redis-master 6379 + +# 4. 查看应用日志 +kubectl logs -f user-rpc-xxx -n juwan +``` + +**解决方案:** +- 确认 Host 配置正确 +- 确认网络策略允许访问 +- 检查 Redis Pod 状态 + +### 2. 认证失败 + +**问题:** `NOAUTH Authentication required` + +**排查:** +```go +// 打印配置(调试用) +func main() { + var c config.Config + conf.MustLoad(*configFile, &c) + + // 检查密码是否正确加载 + fmt.Printf("Redis Config: Host=%s, Pass=%s\n", c.Redis.Host, c.Redis.Pass) +} +``` + +**解决方案:** +- 确认环境变量 `REDIS_PASSWORD` 已设置 +- 确认 Secret 正确挂载 +- 检查密码是否正确 + +### 3. 性能问题 + +**慢查询检测:** +```go +import "time" + +func (l *UserLogic) GetUserWithMetrics(userId int64) (*User, error) { + start := time.Now() + defer func() { + duration := time.Since(start) + if duration > 100*time.Millisecond { + l.Logger.Warnf("Slow Redis query: %v", duration) + } + }() + + // 执行查询 + key := fmt.Sprintf("user:%d", userId) + cached, err := l.svcCtx.Redis.Get(key) + // ... +} +``` + +**常见原因:** +- 连接池耗尽 → 增大 MaxActive +- 大 Value 传输 → 拆分或压缩数据 +- 网络延迟 → 检查网络质量 + +### 4. 内存泄漏 + +**检查连接是否正确关闭:** +```go +// go-zero 的 Redis 客户端会自动管理连接 +// 但如果使用原生 go-redis 客户端,需要手动关闭 + +// ❌ 错误示例 +func bad() { + rdb := redis.NewClient(&redis.Options{...}) + // 使用完后没有关闭 +} + +// ✅ 正确示例 +func good() { + rdb := redis.NewClient(&redis.Options{...}) + defer rdb.Close() + // ... +} +``` + +--- + +## 📖 最佳实践 + +### 1. 环境变量管理 + +**Kubernetes Deployment:** +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-rpc + namespace: juwan +spec: + template: + spec: + containers: + - name: user-rpc + image: user-rpc:v1 + env: + # 从 Secret 读取 Redis 密码 + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: user-redis + key: password + + # 从 ConfigMap 读取其他配置 + - name: REDIS_HOST + valueFrom: + configMapKeyRef: + name: user-rpc-config + key: redis.host +``` + +### 2. 配置分离 + +**开发、测试、生产环境分离:** +```bash +# 开发环境 +go run usercenter.go -f etc/pb-dev.yaml + +# 测试环境 +go run usercenter.go -f etc/pb-test.yaml + +# 生产环境 +./usercenter -f etc/pb-prod.yaml +``` + +### 3. 监控指标 + +```go +import ( + "github.com/zeromicro/go-zero/core/metric" + "github.com/zeromicro/go-zero/core/prometheus" +) + +var ( + redisCacheHit = metric.NewCounterVec(&metric.CounterVecOpts{ + Namespace: "user_rpc", + Subsystem: "redis", + Name: "cache_hit_total", + Help: "redis cache hit count", + Labels: []string{"key"}, + }) + + redisCacheMiss = metric.NewCounterVec(&metric.CounterVecOpts{ + Namespace: "user_rpc", + Subsystem: "redis", + Name: "cache_miss_total", + Help: "redis cache miss count", + Labels: []string{"key"}, + }) +) + +func (l *UserLogic) GetUserWithMetrics(userId int64) (*User, error) { + cacheKey := fmt.Sprintf("user:%d", userId) + + cached, err := l.svcCtx.Redis.Get(cacheKey) + if err == nil && cached != "" { + redisCacheHit.Inc("user") + var user User + json.Unmarshal([]byte(cached), &user) + return &user, nil + } + + redisCacheMiss.Inc("user") + // 查询数据库... +} +``` + +### 4. 错误处理 + +```go +func (l *UserLogic) GetUser(userId int64) (*User, error) { + cacheKey := fmt.Sprintf("user:%d", userId) + + // 缓存查询失败不应该中断流程 + cached, err := l.svcCtx.Redis.Get(cacheKey) + if err == nil && cached != "" { + var user User + if json.Unmarshal([]byte(cached), &user) == nil { + return &user, nil + } + } + + // 缓存失败,降级查询数据库 + user, err := l.getUserFromDB(userId) + if err != nil { + return nil, err + } + + // 尝试回写缓存,失败不影响返回结果 + go func() { + userJSON, _ := json.Marshal(user) + if err := l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600); err != nil { + l.Logger.Errorf("Failed to set cache: %v", err) + } + }() + + return user, nil +} +``` + +### 5. 缓存更新策略 + +**Write-Through(同步更新):** +```go +func (l *UserLogic) UpdateUser(user *User) error { + // 1. 更新数据库 + if err := l.updateUserInDB(user); err != nil { + return err + } + + // 2. 同步更新缓存 + cacheKey := fmt.Sprintf("user:%d", user.Id) + userJSON, _ := json.Marshal(user) + if err := l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600); err != nil { + l.Logger.Errorf("Failed to update cache: %v", err) + } + + return nil +} +``` + +**Write-Behind(异步更新):** +```go +func (l *UserLogic) UpdateUserAsync(user *User) error { + // 1. 立即更新缓存 + cacheKey := fmt.Sprintf("user:%d", user.Id) + userJSON, _ := json.Marshal(user) + l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) + + // 2. 异步更新数据库 + go func() { + if err := l.updateUserInDB(user); err != nil { + l.Logger.Errorf("Failed to update DB: %v", err) + // 回滚缓存 + l.svcCtx.Redis.Del(cacheKey) + } + }() + + return nil +} +``` + +--- + +## 📚 参考资源 + +### 官方文档 +- [go-zero 官方文档](https://go-zero.dev/) +- [go-zero Redis 文档](https://go-zero.dev/docs/tutorials/redis) +- [go-redis 文档](https://redis.uptrace.dev/) + +### 示例代码 +- [go-zero Examples](https://github.com/zeromicro/go-zero/tree/master/example) +- [go-zero Book Store](https://github.com/zeromicro/go-zero-book-store) + +### 相关工具 +- [RedisInsight](https://redis.com/redis-enterprise/redis-insight/) - Redis 管理工具 +- [redis-cli](https://redis.io/docs/manual/cli/) - Redis 命令行工具 + +--- + +## 📝 总结 + +### 快速开始检查清单 + +- [ ] 1. 在 `config.go` 中定义 `Redis redis.RedisConf` +- [ ] 2. 在配置文件中添加 Redis 配置 +- [ ] 3. 在 `ServiceContext` 中初始化 `redis.MustNewRedis(c.Redis)` +- [ ] 4. 在 Kubernetes中配置环境变量 `REDIS_PASSWORD` +- [ ] 5. 在 logic 中使用 `l.svcCtx.Redis` +- [ ] 6. 测试连接是否正常 + +### 生产环境推荐配置 + +```yaml +Redis: + Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Type: sentinel + Pass: ${REDIS_PASSWORD} + MasterName: mymaster + MaxIdle: 16 + MaxActive: 100 + ConnectTimeout: 5000 + ReadTimeout: 3000 + WriteTimeout: 3000 +``` + +### 关键点提醒 + +1. ✅ **生产环境必须使用 Sentinel 模式** +2. ✅ **密码通过环境变量传递,不要硬编码** +3. ✅ **合理设置过期时间,防止缓存雪崩** +4. ✅ **使用 Pipeline 优化批量操作** +5. ✅ **实现缓存降级策略,Redis 故障不影响主流程** + +--- + +**文档版本:** 1.0 +**创建日期:** 2026年2月22日 +**维护者:** Backend Team +**下次审查:** 2026年3月22日 diff --git a/docs/kubernetes-service-explanation.md b/docs/kubernetes-service-explanation.md index 941e0c9..65c2df3 100644 --- a/docs/kubernetes-service-explanation.md +++ b/docs/kubernetes-service-explanation.md @@ -1,743 +1,743 @@ -# Redis Kubernetes Service 详细解析 - -**问题:** 为什么 Redis 有 8 个 Service,但应用配置中只使用 `user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379`? - -**日期:** 2026年2月22日 - ---- - -## 📋 目录 - -1. [Service 概览](#service-概览) -2. [Kubernetes Service 基础](#kubernetes-service-基础) -3. [8 个 Service 的详细说明](#8-个-service-的详细说明) -4. [为什么使用哪个 Service](#为什么使用哪个-service) -5. [Service 创建原理](#service-创建原理) -6. [网络流量路由](#网络流量路由) -7. [故障排查](#故障排查) - ---- - -## 📊 Service 概览 - -### 当前 Redis 的 8 个 Service - -```bash -$ kubectl get svc -n juwan | grep redis - -NAME TYPE CLUSTER-IP PORTS -user-redis ClusterIP 10.103.91.84 6379/TCP,9121/TCP 33m -user-redis-additional ClusterIP 10.107.228.48 6379/TCP 33m -user-redis-headless ClusterIP None 6379/TCP 33m -user-redis-master ClusterIP 10.97.120.76 6379/TCP 33m -user-redis-replica ClusterIP 10.100.213.103 6379/TCP 33m -user-redis-sentinel-sentinel ClusterIP 10.105.28.231 26379/TCP 32m -user-redis-sentinel-sentinel-additional ClusterIP 10.97.111.42 26379/TCP 32m -user-redis-sentinel-sentinel-headless ClusterIP None 26379/TCP 32m -``` - -### 按功能分类 - -| 分类 | Service 名称 | 作用 | -|-----|-------------|------| -| **Redis 数据层** | user-redis | 通用入口 | -| | user-redis-additional | 备用入口 | -| | user-redis-master | 主节点专用 | -| | user-redis-replica | 从节点专用 | -| | user-redis-headless | Pod 间通信 | -| **Sentinel 监控层** | user-redis-sentinel-sentinel | Sentinel 入口 ⭐ | -| | user-redis-sentinel-sentinel-additional | 备用入口 | -| | user-redis-sentinel-sentinel-headless | Sentinel 间通信 | - ---- - -## 🔷 Kubernetes Service 基础 - -### Service 的作用 - -**Kubernetes 中的 Service 是什么?** - -``` -┌─────────────────────────────────────────────────┐ -│ Kubernetes Cluster │ -│ │ -│ Service (虚拟 IP + DNS) │ -│ ↓ │ -│ Endpoints (实际 Pod IP 列表) │ -│ ├─ 10.244.0.10:6379 (Pod 1) │ -│ ├─ 10.244.1.20:6379 (Pod 2) │ -│ └─ 10.244.2.30:6379 (Pod 3) │ -│ │ -│ 客户端 ──→ Service IP (稳定) ──→ Pod IP (变化) │ -└─────────────────────────────────────────────────┘ -``` - -### Service 的三种类型 - -| 类型 | CLUSTER-IP | 用途 | 示例 | -|-----|-----------|------|------| -| **ClusterIP** | ✅ 有 | 集群内访问 | 10.103.91.84 | -| **ClusterIP
(Headless)** | ❌ None | Pod 间直接通信 | None | -| **NodePort** | ✅ 有 | 集群外访问 | 10.103.91.84 | - ---- - -## 🔍 8 个 Service 的详细说明 - -### 第一组:Redis 数据层 Service(端口 6379) - -#### 1️⃣ user-redis(ClusterIP) - -**基本信息:** -```yaml -名称: user-redis -类型: ClusterIP (有负载均衡) -Cluster IP: 10.103.91.84 -端口: 6379/TCP, 9121/TCP -DNS: user-redis.juwan.svc.cluster.local -``` - -**Endpoints 信息:** -```bash -$ kubectl get endpoints user-redis -n juwan - -NAME ENDPOINTS -user-redis 10.244.0.10:6379,10.244.1.20:6379,10.244.2.30:6379 -``` - -**负载均衡机制:** -``` -客户端请求 ──→ Service IP (10.103.91.84) - ↓ - kube-proxy (iptables/ipvs) - ↓ - 随机选择一个 Pod - ├─ 10.244.0.10 (redis-0) - ├─ 10.244.1.20 (redis-1) ← 可能 - └─ 10.244.2.30 (redis-2) -``` - -**特点:** -- ✅ 对所有 Pod 轮询负载均衡 -- ✅ 包含 Redis 数据服务(6379)和 Exporter(9121) -- ⚠️ 可能把写请求轮询到从节点导致失败 - -**适用场景:** -- 监控抓取(Prometheus 从 9121 端口抓指标) -- 不关心读写分离的简单查询 - -**为什么有 2 个端口?** -``` -6379: Redis 数据服务 -9121: Prometheus Exporter 监控端口 - └─ 暴露 Redis 性能指标给 Prometheus - (redis_up, redis_memory_used, etc.) -``` - -**不用这个的原因:** -``` -❌ 如果直接使用 user-redis 进行读写: - ├─ 写请求可能被路由到从节点 (error) - ├─ 无法进行故障自动转移 - └─ 依赖于手动更新配置 -``` - ---- - -#### 2️⃣ user-redis-additional(ClusterIP) - -**基本信息:** -```yaml -名称: user-redis-additional -类型: ClusterIP (有负载均衡) -Cluster IP: 10.107.228.48 -端口: 6379/TCP -Endpoints: 同 user-redis -``` - -**作用:** -- 功能完全同 `user-redis` -- 提供额外的访问入口 -- 用于多租户/网络隔离场景 - -**为什么有这个?** -``` -场景:某些网络策略可能只允许访问特定 Service -└─ 额外的 Service 提供备用入口 -``` - -**不常用的原因:** -- 大多数场景用 `user-redis` 就足够 -- `user-redis-additional` 是备用 - ---- - -#### 3️⃣ user-redis-headless(ClusterIP: None) - -**基本信息:** -```yaml -名称: user-redis-headless -类型: ClusterIP (Headless Service) -Cluster IP: None ← 关键:无虚拟 IP -端口: 6379/TCP -DNS: user-redis-headless.juwan.svc.cluster.local -``` - -**特殊之处:无虚拟 IP** - -```bash -# 正常 Service 查询返回虚拟 IP -$ nslookup user-redis.juwan.svc.cluster.local -Name: user-redis.juwan.svc.cluster.local -Address: 10.103.91.84 ← 虚拟 IP - -# Headless Service 查询返回所有 Pod IP -$ nslookup user-redis-headless.juwan.svc.cluster.local -Name: user-redis-headless.juwan.svc.cluster.local -Address: 10.244.0.10 ← Pod 1 实际 IP -Address: 10.244.1.20 ← Pod 2 实际 IP -Address: 10.244.2.30 ← Pod 3 实际 IP -``` - -**使用场景:** - -``` -┌─────────────────────────────────────────────────┐ -│ StatefulSet (Redis Cluster/Replication) │ -│ │ -│ redis-0 (主) redis-1 (从) redis-2 (从) │ -│ ↓ ↓ ↓ │ -│ 10.244.0.10 10.244.1.20 10.244.2.30 │ -│ ↑ │ -│ 需要直接连接到特定 Pod: │ -│ redis-0.user-redis-headless (连接主节点) │ -│ redis-1.user-redis-headless (连接从节点) │ -└─────────────────────────────────────────────────┘ -``` - -**谁在使用?** -- Redis 主从复制:从节点需要连接到已知的主节点 -- Sentinel 监控:需要直接访问特定 Redis 实例 -- Redis Operator 内部使用 - -**为什么应用不用这个?** -``` -❌ Pod DNS 只能在 Pod 内使用 - └─ 外部应用不知道 Pod 的具体 DNS 名称 - -✅ 用虚拟 Service IP 的优势 - └─ 无需关心底层 Pod 变化 -``` - ---- - -#### 4️⃣ user-redis-master(ClusterIP) - -**基本信息:** -```yaml -名称: user-redis-master -类型: ClusterIP -Cluster IP: 10.97.120.76 -端口: 6379/TCP -Endpoints: 10.244.0.10:6379 (只有 1 个 Pod) -DNS: user-redis-master.juwan.svc.cluster.local -``` - -**特点:只指向主节点** - -```bash -$ kubectl get endpoints user-redis-master -n juwan - -NAME ENDPOINTS -user-redis-master 10.244.0.10:6379 ← 仅主节点 -``` - -**对比所有 Endpoints:** -``` -user-redis-master: 10.244.0.10 (主) -user-redis-replica: 10.244.1.20, 10.244.2.30 (从) -user-redis: 所有 Pod -``` - -**为什么分开?** -``` -┌─────────────────────────────────────────┐ -│ Redis 主从架构 │ -│ │ -│ Redis Master (10.244.0.10) │ -│ ├─ 处理所有写操作 │ -│ └─ 赋值数据给 Slave │ -│ │ -│ Redis Slave 1 (10.244.1.20) │ -│ └─ 处理只读操作 │ -│ │ -│ Redis Slave 2 (10.244.2.30) │ -│ └─ 处理只读操作 │ -└─────────────────────────────────────────┘ - -请求分类: -┌───────────────────────┐ -│ SET key value │ ──→ user-redis-master (10.97.120.76) -│ HSET user:1 name john │ -└───────────────────────┘ - -┌───────────────────────┐ -│ GET key │ ──→ user-redis-replica (10.100.213.103) -│ HGET user:1 name │ -└───────────────────────┘ -``` - -**适用场景:** -- ✅ 读写分离架构 -- ✅ 优化读性能(从节点处理读) -- ✅ 减轻主节点负担 - -**为什么应用通常不直接用?** -``` -❌ 需要在应用层面区分读写操作 - ├─ 写操作 → user-redis-master - ├─ 只读操作 → user-redis-replica - └─ 代码复杂度高 - -✅ Sentinel 模式自动处理 - └─ 应用无需关心主从区别 -``` - ---- - -#### 5️⃣ user-redis-replica(ClusterIP) - -**基本信息:** -```yaml -名称: user-redis-replica -类型: ClusterIP -Cluster IP: 10.100.213.103 -端口: 6379/TCP -Endpoints: 10.244.1.20:6379, 10.244.2.30:6379 (两个从节点) -DNS: user-redis-replica.juwan.svc.cluster.local -``` - -**特点:只指向从节点,支持负载均衡** - -```bash -$ kubectl get endpoints user-redis-replica -n juwan - -NAME ENDPOINTS -user-redis-replica 10.244.1.20:6379, 10.244.2.30:6379 -``` - -**读流量分散:** -``` -应用发送 GET 请求 - ↓ - user-redis-replica (10.100.213.103) - ↓ - 随机选择一个从节点 - ├─ 10.244.1.20 (redis-1) ← 可能 - └─ 10.244.2.30 (redis-2) ← 可能 -``` - -**适用场景:** -- 除了 Sentinel 模式外的读优化 -- 需要手动管理读写分离 - ---- - -### 第二组:Sentinel 监控层 Service(端口 26379) - -#### 6️⃣ user-redis-sentinel-sentinel(ClusterIP)⭐⭐⭐ - -**基本信息:** -```yaml -名称: user-redis-sentinel-sentinel -类型: ClusterIP -Cluster IP: 10.105.28.231 -端口: 26379/TCP -Endpoints: 10.244.0.50:26379, 10.244.1.70:26379, 10.244.2.90:26379 - (3 个 Sentinel 实例) -DNS: user-redis-sentinel-sentinel.juwan.svc.cluster.local -``` - -**为什么应用使用这个?** - -``` -应用程序配置: -┌──────────────────────────────────────────────┐ -│ Redis: │ -│ Host: user-redis-sentinel-sentinel │ -│ Port: 26379 │ -│ Type: sentinel │ -│ MasterName: mymaster │ -└──────────────────────────────────────────────┘ - -连接流程: -┌─────────────────────────────────────────────┐ -│ 应用程序 │ -└────────────────────┬────────────────────────┘ - │ - ↓ -┌─────────────────────────────────────────────┐ -│ user-redis-sentinel-sentinel (26379) │ -│ ├─ Sentinel 1: 10.244.0.50:26379 │ -│ ├─ Sentinel 2: 10.244.1.70:26379 │ -│ └─ Sentinel 3: 10.244.2.90:26379 │ -└────────────────────┬────────────────────────┘ - │ - 应用询问: "mymaster 在哪?" - ↓ - Sentinel 回答: "在 10.244.0.10:6379" - ↓ -┌─────────────────────────────────────────────┐ -│ Redis Master: 10.244.0.10:6379 │ -│ (应用直接连接进行读写) │ -└─────────────────────────────────────────────┘ - -故障转移过程: -Master 故障 → Sentinel 检测 → 提升新主节点 - → 应用下次查询时 → 获得新主节点 IP - → 自动连接新主节点 -``` - -**为什么这是最佳选择?** - -1. **自动故障转移** - ``` - 主节点宕机 (✗) → Sentinel 自动选举新主 → 应用自动连接 - ``` - -2. **高可用** - ``` - Sentinel 集群(3 个) → 任意 1-2 个故障仍可用 - ``` - -3. **应用无感知** - ``` - 应用只需配置 MasterName: mymaster - 无需关心主从地址变化 - ``` - -4. **标准做法** - ``` - ✅ 业界公认的 Redis 高可用方案 - ✅ 最小化应用改动 - ✅ 自动化程度最高 - ``` - -**为什么不用其他 Service?** - -``` -❌ user-redis-master/user-redis-replica - └─ 需要应用层区分读写,主从切换需要重启应用 - -❌ user-redis/user-redis-additional - └─ 没有故障转移能力,故障时应用会报错 - -✅ user-redis-sentinel-sentinel - └─ 自动发现新主节点,无需重启应用 -``` - ---- - -#### 7️⃣ user-redis-sentinel-sentinel-additional(ClusterIP) - -**说明:** 功能同 `user-redis-sentinel-sentinel`,备用入口 - ---- - -#### 8️⃣ user-redis-sentinel-sentinel-headless(ClusterIP: None) - -**说明:** 供 Sentinel 内部通信和选举使用 - ---- - -## 🎯 为什么使用哪个 Service - -### 应用配置选择 - -#### ⭐⭐⭐ Sentinel 模式(生产推荐) - -```yaml -# 应用配置 -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - MasterName: mymaster - Pass: ${REDIS_PASSWORD} -``` - -**优势:** -- ✅ 自动故障转移(RTO < 30 秒) -- ✅ 应用无需重启 -- ✅ 自动发现新主节点 -- ✅ 生产标准做法 - ---- - -#### ⭐⭐ 主从分离模式(可选) - -```yaml -# 应用配置(需要两个 host) -Redis: - Master: - Host: user-redis-master.juwan.svc.cluster.local:6379 - Slave: - Host: user-redis-replica.juwan.svc.cluster.local:6379 -``` - -**适用场景:** -- 读写分离显著 -- 对读性能有极高要求 - -**缺点:** -- 主从故障需手动切换 -- 应用层复杂度高 - ---- - -#### ❌ 不推荐的做法 - -```yaml -# ❌ 直接连接单个节点 -Redis: - Host: user-redis-0.user-redis-headless.juwan.svc.cluster.local:6379 - # 问题:Pod 重启 IP 变化,需要更新配置 - -# ❌ 连接通用 Service(无故障转移) -Redis: - Host: user-redis.juwan.svc.cluster.local:6379 - # 问题:无法自动转移,故障时应用报错 - -# ❌ 硬编码 Pod IP -Redis: - Host: 10.244.0.10:6379 - # 问题:Pod 重启 IP 变化,应用立即不可用 -``` - ---- - -## 🔌 Service 创建原理 - -### 为什么会自动创建这么多 Service? - -**由 Redis Operator 自动创建:** - -```go -// Redis Operator 逻辑(伪代码) -func CreateServicesForRedis(redis *RedisReplication) { - // 数据层 Service - CreateService("user-redis", AllRedisNodes) - CreateService("user-redis-additional", AllRedisNodes) - CreateService("user-redis-master", [MasterNode]) - CreateService("user-redis-replica", [SlaveNodes]) - CreateHeadlessService("user-redis-headless", AllRedisNodes) - - // 监控层 Service - CreateService("user-redis-sentinel-sentinel", AllSentinelNodes) - CreateService("user-redis-sentinel-sentinel-additional", AllSentinelNodes) - CreateHeadlessService("user-redis-sentinel-sentinel-headless", AllSentinelNodes) -} -``` - -**为什么这样设计?** - -| Service | 原因 | -|---------|------| -| 多个 ClusterIP | 不同场景需要不同的 Endpoints 配置 | -| 包含 additional | 网络隔离/多租户支持 | -| 包含 headless | StatefulSet 需要 Pod 间直接通信 | - -**类比:** -``` -Redis Operator 就像一个完整的产品 -└─ 提供多种方式使用 Redis - ├─ 简单: user-redis - ├─ 高级: user-redis-master/replica - ├─ HA: user-redis-sentinel-sentinel - └─ 内部: headless services -``` - ---- - -## 🌐 网络流量路由 - -### 查询 Service 背后的 Pod - -**查看 Service Endpoints:** - -```bash -# 查看 user-redis 关联的 Pod -$ kubectl get endpoints user-redis -n juwan -NAME ENDPOINTS -user-redis 10.244.0.10:6379,10.244.1.20:6379,10.244.2.30:6379 - -# 查看 user-redis-master 关联的 Pod -$ kubectl get endpoints user-redis-master -n juwan -NAME ENDPOINTS -user-redis-master 10.244.0.10:6379 - -# 查看 user-redis-replica 关联的 Pod -$ kubectl get endpoints user-redis-replica -n juwan -NAME ENDPOINTS -user-redis-replica 10.244.1.20:6379,10.244.2.30:6379 -``` - -**Pod 和 Service 的映射关系:** - -``` -Pods (实际运行的实例) Services (虚拟 IP) -└─ redis-0 (主) └─ user-redis (所有) - ├─ 10.244.0.10 ├─ 10.103.91.84 - └─ :6379 - └─ user-redis-master (仅主) -└─ redis-1 (从) ├─ 10.97.120.76 - ├─ 10.244.1.20 - └─ :6379 - └─ user-redis-replica (仅从) -└─ redis-2 (从) ├─ 10.100.213.103 - ├─ 10.244.2.30 - └─ :6379 -``` - -**DNS 解析过程:** - -``` -应用 DNS 查询 - └─ user-redis-master.juwan.svc.cluster.local - ↓ -CoreDNS (Kubernetes DNS) - └─ 查询并返回 Service IP: - ├─ 10.97.120.76 (user-redis-master) - ├─ 或 10.100.213.103 (user-redis-replica) - ├─ 或 10.103.91.84 (user-redis) - └─ 或 Sentinel 的 IP -``` - -**Sentinel 模式的特殊之处:** - -``` -应用查询 Sentinel - └─ user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - ↓ -Sentinel Service (负载均衡到 3 个 Sentinel 节点) - ↓ -Sentinel 节点 (任选一个) - ↓ -应用询问: "mymaster 主节点 IP 是什么?" - ↓ -Sentinel 回答: "10.244.0.10:6379" - ↓ -应用直接连接 Redis Master: 10.244.0.10:6379 -``` - ---- - -## 🔧 故障排查 - -### 问题 1:为什么应用连接失败? - -**检查步骤:** - -```bash -# 1. 验证 Service 存在 -kubectl get svc user-redis-sentinel-sentinel -n juwan - -# 2. 验证 Endpoints 不为空 -kubectl get endpoints user-redis-sentinel-sentinel -n juwan - -# 3. 测试 DNS 解析 -kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \ - nslookup user-redis-sentinel-sentinel.juwan.svc.cluster.local - -# 4. 测试连接性 -kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \ - nc -zv user-redis-sentinel-sentinel.juwan.svc.cluster.local 26379 - -# 5. 查看应用日志 -kubectl logs -f user-rpc-xxx -n juwan -``` - -### 问题 2:为什么看不到某个 Service? - -```bash -# 确保在正确的命名空间 -kubectl get svc -n juwan | grep redis - -# 如果 Redis Operator 有问题,Service 可能不会创建 -# 查看 Operator 日志 -kubectl logs -n default deployment/redis-operator -``` - -### 问题 3:Service IP 经常变化? - -```bash -# Service IP 是稳定的(除非被删除和重建) -# 如果频繁变化,说明 Service 被频繁重建 - -# 检查 Service 创建事件 -kubectl describe svc user-redis-sentinel-sentinel -n juwan - -# 检查 Operator 是否有异常 -kubectl describe redissentinel user-redis-sentinel -n juwan -``` - ---- - -## 📚 总结 - -### 快速理解 - -| Service | 用途 | 应用是否使用 | -|---------|------|-----------| -| **user-redis-sentinel-sentinel** | ⭐ Sentinel 高可用 | ✅ **生产推荐** | -| user-redis-master | 直连主节点 | ⚠️ 需要读写分离 | -| user-redis-replica | 直连从节点 | ⚠️ 需要读写分离 | -| user-redis | 通用入口 | ❌ 不推荐(无 HA) | -| headless services | 内部通信 | ❌ 应用不用 | - -### 为什么有这么多 Service? - -**答案:** 为了提供灵活的使用方式 - -``` -Redis Operator 的设计理念: -┌─────────────────────────────────────────┐ -│ 提供完整的 Redis 高可用解决方案 │ -│ │ -│ ├─ 简单使用场景 │ -│ │ └─ user-redis (所有节点) │ -│ │ │ -│ ├─ 高级使用场景 │ -│ │ ├─ user-redis-master (写) │ -│ │ └─ user-redis-replica (读) │ -│ │ │ -│ ├─ 生产场景 (推荐) │ -│ │ └─ user-redis-sentinel-sentinel │ -│ │ │ -│ └─ 内部通信 │ -│ └─ headless services │ -└─────────────────────────────────────────┘ -``` - -### 应用该用哪个? - -**一句话:使用 `user-redis-sentinel-sentinel:26379` + Sentinel 模式** - -```yaml -# 这是最佳实践 -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - MasterName: mymaster -``` - -**为什么?** -- ✅ 自动故障转移 -- ✅ 应用无需重启 -- ✅ 无需手工干预 -- ✅ 行业标准 - ---- - -**文档版本:** 1.0 -**创建日期:** 2026年2月22日 -**维护者:** DevOps Team +# Redis Kubernetes Service 详细解析 + +**问题:** 为什么 Redis 有 8 个 Service,但应用配置中只使用 `user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379`? + +**日期:** 2026年2月22日 + +--- + +## 📋 目录 + +1. [Service 概览](#service-概览) +2. [Kubernetes Service 基础](#kubernetes-service-基础) +3. [8 个 Service 的详细说明](#8-个-service-的详细说明) +4. [为什么使用哪个 Service](#为什么使用哪个-service) +5. [Service 创建原理](#service-创建原理) +6. [网络流量路由](#网络流量路由) +7. [故障排查](#故障排查) + +--- + +## 📊 Service 概览 + +### 当前 Redis 的 8 个 Service + +```bash +$ kubectl get svc -n juwan | grep redis + +NAME TYPE CLUSTER-IP PORTS +user-redis ClusterIP 10.103.91.84 6379/TCP,9121/TCP 33m +user-redis-additional ClusterIP 10.107.228.48 6379/TCP 33m +user-redis-headless ClusterIP None 6379/TCP 33m +user-redis-master ClusterIP 10.97.120.76 6379/TCP 33m +user-redis-replica ClusterIP 10.100.213.103 6379/TCP 33m +user-redis-sentinel-sentinel ClusterIP 10.105.28.231 26379/TCP 32m +user-redis-sentinel-sentinel-additional ClusterIP 10.97.111.42 26379/TCP 32m +user-redis-sentinel-sentinel-headless ClusterIP None 26379/TCP 32m +``` + +### 按功能分类 + +| 分类 | Service 名称 | 作用 | +|-----|-------------|------| +| **Redis 数据层** | user-redis | 通用入口 | +| | user-redis-additional | 备用入口 | +| | user-redis-master | 主节点专用 | +| | user-redis-replica | 从节点专用 | +| | user-redis-headless | Pod 间通信 | +| **Sentinel 监控层** | user-redis-sentinel-sentinel | Sentinel 入口 ⭐ | +| | user-redis-sentinel-sentinel-additional | 备用入口 | +| | user-redis-sentinel-sentinel-headless | Sentinel 间通信 | + +--- + +## 🔷 Kubernetes Service 基础 + +### Service 的作用 + +**Kubernetes 中的 Service 是什么?** + +``` +┌─────────────────────────────────────────────────┐ +│ Kubernetes Cluster │ +│ │ +│ Service (虚拟 IP + DNS) │ +│ ↓ │ +│ Endpoints (实际 Pod IP 列表) │ +│ ├─ 10.244.0.10:6379 (Pod 1) │ +│ ├─ 10.244.1.20:6379 (Pod 2) │ +│ └─ 10.244.2.30:6379 (Pod 3) │ +│ │ +│ 客户端 ──→ Service IP (稳定) ──→ Pod IP (变化) │ +└─────────────────────────────────────────────────┘ +``` + +### Service 的三种类型 + +| 类型 | CLUSTER-IP | 用途 | 示例 | +|-----|-----------|------|------| +| **ClusterIP** | ✅ 有 | 集群内访问 | 10.103.91.84 | +| **ClusterIP
(Headless)** | ❌ None | Pod 间直接通信 | None | +| **NodePort** | ✅ 有 | 集群外访问 | 10.103.91.84 | + +--- + +## 🔍 8 个 Service 的详细说明 + +### 第一组:Redis 数据层 Service(端口 6379) + +#### 1️⃣ user-redis(ClusterIP) + +**基本信息:** +```yaml +名称: user-redis +类型: ClusterIP (有负载均衡) +Cluster IP: 10.103.91.84 +端口: 6379/TCP, 9121/TCP +DNS: user-redis.juwan.svc.cluster.local +``` + +**Endpoints 信息:** +```bash +$ kubectl get endpoints user-redis -n juwan + +NAME ENDPOINTS +user-redis 10.244.0.10:6379,10.244.1.20:6379,10.244.2.30:6379 +``` + +**负载均衡机制:** +``` +客户端请求 ──→ Service IP (10.103.91.84) + ↓ + kube-proxy (iptables/ipvs) + ↓ + 随机选择一个 Pod + ├─ 10.244.0.10 (redis-0) + ├─ 10.244.1.20 (redis-1) ← 可能 + └─ 10.244.2.30 (redis-2) +``` + +**特点:** +- ✅ 对所有 Pod 轮询负载均衡 +- ✅ 包含 Redis 数据服务(6379)和 Exporter(9121) +- ⚠️ 可能把写请求轮询到从节点导致失败 + +**适用场景:** +- 监控抓取(Prometheus 从 9121 端口抓指标) +- 不关心读写分离的简单查询 + +**为什么有 2 个端口?** +``` +6379: Redis 数据服务 +9121: Prometheus Exporter 监控端口 + └─ 暴露 Redis 性能指标给 Prometheus + (redis_up, redis_memory_used, etc.) +``` + +**不用这个的原因:** +``` +❌ 如果直接使用 user-redis 进行读写: + ├─ 写请求可能被路由到从节点 (error) + ├─ 无法进行故障自动转移 + └─ 依赖于手动更新配置 +``` + +--- + +#### 2️⃣ user-redis-additional(ClusterIP) + +**基本信息:** +```yaml +名称: user-redis-additional +类型: ClusterIP (有负载均衡) +Cluster IP: 10.107.228.48 +端口: 6379/TCP +Endpoints: 同 user-redis +``` + +**作用:** +- 功能完全同 `user-redis` +- 提供额外的访问入口 +- 用于多租户/网络隔离场景 + +**为什么有这个?** +``` +场景:某些网络策略可能只允许访问特定 Service +└─ 额外的 Service 提供备用入口 +``` + +**不常用的原因:** +- 大多数场景用 `user-redis` 就足够 +- `user-redis-additional` 是备用 + +--- + +#### 3️⃣ user-redis-headless(ClusterIP: None) + +**基本信息:** +```yaml +名称: user-redis-headless +类型: ClusterIP (Headless Service) +Cluster IP: None ← 关键:无虚拟 IP +端口: 6379/TCP +DNS: user-redis-headless.juwan.svc.cluster.local +``` + +**特殊之处:无虚拟 IP** + +```bash +# 正常 Service 查询返回虚拟 IP +$ nslookup user-redis.juwan.svc.cluster.local +Name: user-redis.juwan.svc.cluster.local +Address: 10.103.91.84 ← 虚拟 IP + +# Headless Service 查询返回所有 Pod IP +$ nslookup user-redis-headless.juwan.svc.cluster.local +Name: user-redis-headless.juwan.svc.cluster.local +Address: 10.244.0.10 ← Pod 1 实际 IP +Address: 10.244.1.20 ← Pod 2 实际 IP +Address: 10.244.2.30 ← Pod 3 实际 IP +``` + +**使用场景:** + +``` +┌─────────────────────────────────────────────────┐ +│ StatefulSet (Redis Cluster/Replication) │ +│ │ +│ redis-0 (主) redis-1 (从) redis-2 (从) │ +│ ↓ ↓ ↓ │ +│ 10.244.0.10 10.244.1.20 10.244.2.30 │ +│ ↑ │ +│ 需要直接连接到特定 Pod: │ +│ redis-0.user-redis-headless (连接主节点) │ +│ redis-1.user-redis-headless (连接从节点) │ +└─────────────────────────────────────────────────┘ +``` + +**谁在使用?** +- Redis 主从复制:从节点需要连接到已知的主节点 +- Sentinel 监控:需要直接访问特定 Redis 实例 +- Redis Operator 内部使用 + +**为什么应用不用这个?** +``` +❌ Pod DNS 只能在 Pod 内使用 + └─ 外部应用不知道 Pod 的具体 DNS 名称 + +✅ 用虚拟 Service IP 的优势 + └─ 无需关心底层 Pod 变化 +``` + +--- + +#### 4️⃣ user-redis-master(ClusterIP) + +**基本信息:** +```yaml +名称: user-redis-master +类型: ClusterIP +Cluster IP: 10.97.120.76 +端口: 6379/TCP +Endpoints: 10.244.0.10:6379 (只有 1 个 Pod) +DNS: user-redis-master.juwan.svc.cluster.local +``` + +**特点:只指向主节点** + +```bash +$ kubectl get endpoints user-redis-master -n juwan + +NAME ENDPOINTS +user-redis-master 10.244.0.10:6379 ← 仅主节点 +``` + +**对比所有 Endpoints:** +``` +user-redis-master: 10.244.0.10 (主) +user-redis-replica: 10.244.1.20, 10.244.2.30 (从) +user-redis: 所有 Pod +``` + +**为什么分开?** +``` +┌─────────────────────────────────────────┐ +│ Redis 主从架构 │ +│ │ +│ Redis Master (10.244.0.10) │ +│ ├─ 处理所有写操作 │ +│ └─ 赋值数据给 Slave │ +│ │ +│ Redis Slave 1 (10.244.1.20) │ +│ └─ 处理只读操作 │ +│ │ +│ Redis Slave 2 (10.244.2.30) │ +│ └─ 处理只读操作 │ +└─────────────────────────────────────────┘ + +请求分类: +┌───────────────────────┐ +│ SET key value │ ──→ user-redis-master (10.97.120.76) +│ HSET user:1 name john │ +└───────────────────────┘ + +┌───────────────────────┐ +│ GET key │ ──→ user-redis-replica (10.100.213.103) +│ HGET user:1 name │ +└───────────────────────┘ +``` + +**适用场景:** +- ✅ 读写分离架构 +- ✅ 优化读性能(从节点处理读) +- ✅ 减轻主节点负担 + +**为什么应用通常不直接用?** +``` +❌ 需要在应用层面区分读写操作 + ├─ 写操作 → user-redis-master + ├─ 只读操作 → user-redis-replica + └─ 代码复杂度高 + +✅ Sentinel 模式自动处理 + └─ 应用无需关心主从区别 +``` + +--- + +#### 5️⃣ user-redis-replica(ClusterIP) + +**基本信息:** +```yaml +名称: user-redis-replica +类型: ClusterIP +Cluster IP: 10.100.213.103 +端口: 6379/TCP +Endpoints: 10.244.1.20:6379, 10.244.2.30:6379 (两个从节点) +DNS: user-redis-replica.juwan.svc.cluster.local +``` + +**特点:只指向从节点,支持负载均衡** + +```bash +$ kubectl get endpoints user-redis-replica -n juwan + +NAME ENDPOINTS +user-redis-replica 10.244.1.20:6379, 10.244.2.30:6379 +``` + +**读流量分散:** +``` +应用发送 GET 请求 + ↓ + user-redis-replica (10.100.213.103) + ↓ + 随机选择一个从节点 + ├─ 10.244.1.20 (redis-1) ← 可能 + └─ 10.244.2.30 (redis-2) ← 可能 +``` + +**适用场景:** +- 除了 Sentinel 模式外的读优化 +- 需要手动管理读写分离 + +--- + +### 第二组:Sentinel 监控层 Service(端口 26379) + +#### 6️⃣ user-redis-sentinel-sentinel(ClusterIP)⭐⭐⭐ + +**基本信息:** +```yaml +名称: user-redis-sentinel-sentinel +类型: ClusterIP +Cluster IP: 10.105.28.231 +端口: 26379/TCP +Endpoints: 10.244.0.50:26379, 10.244.1.70:26379, 10.244.2.90:26379 + (3 个 Sentinel 实例) +DNS: user-redis-sentinel-sentinel.juwan.svc.cluster.local +``` + +**为什么应用使用这个?** + +``` +应用程序配置: +┌──────────────────────────────────────────────┐ +│ Redis: │ +│ Host: user-redis-sentinel-sentinel │ +│ Port: 26379 │ +│ Type: sentinel │ +│ MasterName: mymaster │ +└──────────────────────────────────────────────┘ + +连接流程: +┌─────────────────────────────────────────────┐ +│ 应用程序 │ +└────────────────────┬────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────┐ +│ user-redis-sentinel-sentinel (26379) │ +│ ├─ Sentinel 1: 10.244.0.50:26379 │ +│ ├─ Sentinel 2: 10.244.1.70:26379 │ +│ └─ Sentinel 3: 10.244.2.90:26379 │ +└────────────────────┬────────────────────────┘ + │ + 应用询问: "mymaster 在哪?" + ↓ + Sentinel 回答: "在 10.244.0.10:6379" + ↓ +┌─────────────────────────────────────────────┐ +│ Redis Master: 10.244.0.10:6379 │ +│ (应用直接连接进行读写) │ +└─────────────────────────────────────────────┘ + +故障转移过程: +Master 故障 → Sentinel 检测 → 提升新主节点 + → 应用下次查询时 → 获得新主节点 IP + → 自动连接新主节点 +``` + +**为什么这是最佳选择?** + +1. **自动故障转移** + ``` + 主节点宕机 (✗) → Sentinel 自动选举新主 → 应用自动连接 + ``` + +2. **高可用** + ``` + Sentinel 集群(3 个) → 任意 1-2 个故障仍可用 + ``` + +3. **应用无感知** + ``` + 应用只需配置 MasterName: mymaster + 无需关心主从地址变化 + ``` + +4. **标准做法** + ``` + ✅ 业界公认的 Redis 高可用方案 + ✅ 最小化应用改动 + ✅ 自动化程度最高 + ``` + +**为什么不用其他 Service?** + +``` +❌ user-redis-master/user-redis-replica + └─ 需要应用层区分读写,主从切换需要重启应用 + +❌ user-redis/user-redis-additional + └─ 没有故障转移能力,故障时应用会报错 + +✅ user-redis-sentinel-sentinel + └─ 自动发现新主节点,无需重启应用 +``` + +--- + +#### 7️⃣ user-redis-sentinel-sentinel-additional(ClusterIP) + +**说明:** 功能同 `user-redis-sentinel-sentinel`,备用入口 + +--- + +#### 8️⃣ user-redis-sentinel-sentinel-headless(ClusterIP: None) + +**说明:** 供 Sentinel 内部通信和选举使用 + +--- + +## 🎯 为什么使用哪个 Service + +### 应用配置选择 + +#### ⭐⭐⭐ Sentinel 模式(生产推荐) + +```yaml +# 应用配置 +Redis: + Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Type: sentinel + MasterName: mymaster + Pass: ${REDIS_PASSWORD} +``` + +**优势:** +- ✅ 自动故障转移(RTO < 30 秒) +- ✅ 应用无需重启 +- ✅ 自动发现新主节点 +- ✅ 生产标准做法 + +--- + +#### ⭐⭐ 主从分离模式(可选) + +```yaml +# 应用配置(需要两个 host) +Redis: + Master: + Host: user-redis-master.juwan.svc.cluster.local:6379 + Slave: + Host: user-redis-replica.juwan.svc.cluster.local:6379 +``` + +**适用场景:** +- 读写分离显著 +- 对读性能有极高要求 + +**缺点:** +- 主从故障需手动切换 +- 应用层复杂度高 + +--- + +#### ❌ 不推荐的做法 + +```yaml +# ❌ 直接连接单个节点 +Redis: + Host: user-redis-0.user-redis-headless.juwan.svc.cluster.local:6379 + # 问题:Pod 重启 IP 变化,需要更新配置 + +# ❌ 连接通用 Service(无故障转移) +Redis: + Host: user-redis.juwan.svc.cluster.local:6379 + # 问题:无法自动转移,故障时应用报错 + +# ❌ 硬编码 Pod IP +Redis: + Host: 10.244.0.10:6379 + # 问题:Pod 重启 IP 变化,应用立即不可用 +``` + +--- + +## 🔌 Service 创建原理 + +### 为什么会自动创建这么多 Service? + +**由 Redis Operator 自动创建:** + +```go +// Redis Operator 逻辑(伪代码) +func CreateServicesForRedis(redis *RedisReplication) { + // 数据层 Service + CreateService("user-redis", AllRedisNodes) + CreateService("user-redis-additional", AllRedisNodes) + CreateService("user-redis-master", [MasterNode]) + CreateService("user-redis-replica", [SlaveNodes]) + CreateHeadlessService("user-redis-headless", AllRedisNodes) + + // 监控层 Service + CreateService("user-redis-sentinel-sentinel", AllSentinelNodes) + CreateService("user-redis-sentinel-sentinel-additional", AllSentinelNodes) + CreateHeadlessService("user-redis-sentinel-sentinel-headless", AllSentinelNodes) +} +``` + +**为什么这样设计?** + +| Service | 原因 | +|---------|------| +| 多个 ClusterIP | 不同场景需要不同的 Endpoints 配置 | +| 包含 additional | 网络隔离/多租户支持 | +| 包含 headless | StatefulSet 需要 Pod 间直接通信 | + +**类比:** +``` +Redis Operator 就像一个完整的产品 +└─ 提供多种方式使用 Redis + ├─ 简单: user-redis + ├─ 高级: user-redis-master/replica + ├─ HA: user-redis-sentinel-sentinel + └─ 内部: headless services +``` + +--- + +## 🌐 网络流量路由 + +### 查询 Service 背后的 Pod + +**查看 Service Endpoints:** + +```bash +# 查看 user-redis 关联的 Pod +$ kubectl get endpoints user-redis -n juwan +NAME ENDPOINTS +user-redis 10.244.0.10:6379,10.244.1.20:6379,10.244.2.30:6379 + +# 查看 user-redis-master 关联的 Pod +$ kubectl get endpoints user-redis-master -n juwan +NAME ENDPOINTS +user-redis-master 10.244.0.10:6379 + +# 查看 user-redis-replica 关联的 Pod +$ kubectl get endpoints user-redis-replica -n juwan +NAME ENDPOINTS +user-redis-replica 10.244.1.20:6379,10.244.2.30:6379 +``` + +**Pod 和 Service 的映射关系:** + +``` +Pods (实际运行的实例) Services (虚拟 IP) +└─ redis-0 (主) └─ user-redis (所有) + ├─ 10.244.0.10 ├─ 10.103.91.84 + └─ :6379 + └─ user-redis-master (仅主) +└─ redis-1 (从) ├─ 10.97.120.76 + ├─ 10.244.1.20 + └─ :6379 + └─ user-redis-replica (仅从) +└─ redis-2 (从) ├─ 10.100.213.103 + ├─ 10.244.2.30 + └─ :6379 +``` + +**DNS 解析过程:** + +``` +应用 DNS 查询 + └─ user-redis-master.juwan.svc.cluster.local + ↓ +CoreDNS (Kubernetes DNS) + └─ 查询并返回 Service IP: + ├─ 10.97.120.76 (user-redis-master) + ├─ 或 10.100.213.103 (user-redis-replica) + ├─ 或 10.103.91.84 (user-redis) + └─ 或 Sentinel 的 IP +``` + +**Sentinel 模式的特殊之处:** + +``` +应用查询 Sentinel + └─ user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + ↓ +Sentinel Service (负载均衡到 3 个 Sentinel 节点) + ↓ +Sentinel 节点 (任选一个) + ↓ +应用询问: "mymaster 主节点 IP 是什么?" + ↓ +Sentinel 回答: "10.244.0.10:6379" + ↓ +应用直接连接 Redis Master: 10.244.0.10:6379 +``` + +--- + +## 🔧 故障排查 + +### 问题 1:为什么应用连接失败? + +**检查步骤:** + +```bash +# 1. 验证 Service 存在 +kubectl get svc user-redis-sentinel-sentinel -n juwan + +# 2. 验证 Endpoints 不为空 +kubectl get endpoints user-redis-sentinel-sentinel -n juwan + +# 3. 测试 DNS 解析 +kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \ + nslookup user-redis-sentinel-sentinel.juwan.svc.cluster.local + +# 4. 测试连接性 +kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \ + nc -zv user-redis-sentinel-sentinel.juwan.svc.cluster.local 26379 + +# 5. 查看应用日志 +kubectl logs -f user-rpc-xxx -n juwan +``` + +### 问题 2:为什么看不到某个 Service? + +```bash +# 确保在正确的命名空间 +kubectl get svc -n juwan | grep redis + +# 如果 Redis Operator 有问题,Service 可能不会创建 +# 查看 Operator 日志 +kubectl logs -n default deployment/redis-operator +``` + +### 问题 3:Service IP 经常变化? + +```bash +# Service IP 是稳定的(除非被删除和重建) +# 如果频繁变化,说明 Service 被频繁重建 + +# 检查 Service 创建事件 +kubectl describe svc user-redis-sentinel-sentinel -n juwan + +# 检查 Operator 是否有异常 +kubectl describe redissentinel user-redis-sentinel -n juwan +``` + +--- + +## 📚 总结 + +### 快速理解 + +| Service | 用途 | 应用是否使用 | +|---------|------|-----------| +| **user-redis-sentinel-sentinel** | ⭐ Sentinel 高可用 | ✅ **生产推荐** | +| user-redis-master | 直连主节点 | ⚠️ 需要读写分离 | +| user-redis-replica | 直连从节点 | ⚠️ 需要读写分离 | +| user-redis | 通用入口 | ❌ 不推荐(无 HA) | +| headless services | 内部通信 | ❌ 应用不用 | + +### 为什么有这么多 Service? + +**答案:** 为了提供灵活的使用方式 + +``` +Redis Operator 的设计理念: +┌─────────────────────────────────────────┐ +│ 提供完整的 Redis 高可用解决方案 │ +│ │ +│ ├─ 简单使用场景 │ +│ │ └─ user-redis (所有节点) │ +│ │ │ +│ ├─ 高级使用场景 │ +│ │ ├─ user-redis-master (写) │ +│ │ └─ user-redis-replica (读) │ +│ │ │ +│ ├─ 生产场景 (推荐) │ +│ │ └─ user-redis-sentinel-sentinel │ +│ │ │ +│ └─ 内部通信 │ +│ └─ headless services │ +└─────────────────────────────────────────┘ +``` + +### 应用该用哪个? + +**一句话:使用 `user-redis-sentinel-sentinel:26379` + Sentinel 模式** + +```yaml +# 这是最佳实践 +Redis: + Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Type: sentinel + MasterName: mymaster +``` + +**为什么?** +- ✅ 自动故障转移 +- ✅ 应用无需重启 +- ✅ 无需手工干预 +- ✅ 行业标准 + +--- + +**文档版本:** 1.0 +**创建日期:** 2026年2月22日 +**维护者:** DevOps Team diff --git a/docs/redis-sentinel-troubleshooting.md b/docs/redis-sentinel-troubleshooting.md index 2a555d9..c642b63 100644 --- a/docs/redis-sentinel-troubleshooting.md +++ b/docs/redis-sentinel-troubleshooting.md @@ -1,779 +1,779 @@ -# Redis Sentinel 部署问题诊断与修复报告 - -**问题日期:** 2026年2月22日 -**命名空间:** juwan -**涉及资源:** user-rpc deployment, RedisSentinel - ---- - -## 📋 目录 - -1. [问题背景](#问题背景) -2. [问题现象](#问题现象) -3. [诊断过程](#诊断过程) -4. [根因分析](#根因分析) -5. [解决方案](#解决方案) -6. [修复步骤](#修复步骤) -7. [验证结果](#验证结果) -8. [后续建议](#后续建议) - ---- - -## 🎯 问题背景 - -### 部署目标 -部署一个简单的三节点 Redis Sentinel 哨兵集群作为缓存服务,供 user-rpc 服务使用。后续如有需要再扩展为分片集群。 - -### 初始配置 -在 `deploy/k8s/service/user/user-rpc.yaml` 中配置了: -- user-rpc Deployment(3副本) -- user-rpc Service -- HPA(CPU和内存) -- **RedisSentinel 资源** -- PostgreSQL Cluster - ---- - -## 🔴 问题现象 - -### 执行的操作 -```bash -kubectl apply -f .\deploy\k8s\service\user\user-rpc.yaml -``` - -### 输出结果 -``` -deployment.apps/user-rpc configured -service/user-rpc-svc unchanged -horizontalpodautoscaler.autoscaling/user-rpc-hpa-c unchanged -horizontalpodautoscaler.autoscaling/user-rpc-hpa-m unchanged -redissentinel.redis.redis.opstreelabs.in/user-redis unchanged -cluster.postgresql.cnpg.io/user-db unchanged -``` - -### 观察到的异常 -查看命名空间资源: -```bash -kubectl get all -n juwan -``` - -**发现:** -- ✅ user-api pods 正常运行 -- ✅ user-rpc pods 正常运行 -- ✅ PostgreSQL clusters 正常运行 -- ❌ **没有任何 Redis 相关的 Pod** -- ❌ **没有 Redis Service** - ---- - -## 🔍 诊断过程 - -### 步骤 1:检查 RedisSentinel 资源状态 - -**目的:** 确认 RedisSentinel 资源是否被成功创建 - -**命令:** -```bash -kubectl get redissentinel user-redis -n juwan -``` - -**输出:** -``` -NAME AGE -user-redis 9m56s -``` - -**分析:** -- ✅ RedisSentinel 资源已创建 -- ❌ 但没有创建任何 Pod -- **结论:** Operator 没有按照 RedisSentinel 规格创建实际资源 - ---- - -### 步骤 2:查看 RedisSentinel 详细信息 - -**目的:** 检查资源的详细配置和事件 - -**命令:** -```bash -kubectl describe redissentinel user-redis -n juwan -``` - -**关键输出:** -```yaml -API Version: redis.redis.opstreelabs.in/v1beta2 -Kind: RedisSentinel -Metadata: - Creation Timestamp: 2026-02-22T12:41:47Z - Finalizers: - redisSentinelFinalizer - Generation: 2 -Spec: - Cluster Size: 3 - Redis Sentinel Config: - Redis Replication Name: user-redis # ⚠️ 关键配置 - Master Group Name: myMaster - Redis Port: 6379 - Quorum: 2 -Events: # ⚠️ 没有任何事件 -``` - -**分析:** -- 配置中引用了 `redisReplicationName: user-redis` -- 没有任何 Events,说明 Operator 可能在等待某些前置条件 -- **需要进一步检查依赖资源** - ---- - -### 步骤 3:检查 Secret 资源 - -**目的:** 确认 RedisSentinel 所需的密码 Secret 是否存在 - -**命令:** -```bash -kubectl get secret user-redis -n juwan -``` - -**输出:** -``` -NAME TYPE DATA AGE -user-redis Opaque 1 5h48m -``` - -**分析:** -- ✅ Secret 存在,排除认证配置问题 - ---- - -### 步骤 4:查找 Redis Pods - -**目的:** 确认是否有 Redis Pod 使用不同的命名规则 - -**命令:** -```bash -kubectl get pods -n juwan | Select-String redis -``` - -**输出:** -``` -(空输出 - 没有找到任何 Redis Pod) -``` - -**分析:** -- ❌ 确认没有任何 Redis Pod 被创建 - ---- - -### 步骤 5:检查 StatefulSet - -**目的:** Redis 通常使用 StatefulSet 部署,检查是否有相关资源 - -**命令:** -```bash -kubectl get statefulset -n juwan -``` - -**输出:** -``` -No resources found in juwan namespace. -``` - -**分析:** -- ❌ 没有 StatefulSet 被创建 -- **结论:** Operator 完全没有执行创建动作 - ---- - -### 步骤 6:检查 Redis Operator 状态 - -**目的:** 确认 Redis Operator 本身是否正常运行 - -**命令:** -```bash -kubectl get pods -A | Select-String redis-operator -``` - -**输出:** -``` -default redis-operator-7dcf4468c9-gfbcm 1/1 Running 0 4h50m -ot-operators redis-operator-69bb645f5b-6kxkv 0/1 ErrImageNeverPull 0 4h48m -``` - -**分析:** -- ✅ default namespace 中的 Operator 正常运行 -- ⚠️ ot-operators namespace 中的 Operator 镜像拉取失败(但不影响我们的场景) - ---- - -### 步骤 7:查看 Operator 日志 - -**目的:** 从 Operator 日志中寻找线索 - -**命令:** -```bash -kubectl logs redis-operator-7dcf4468c9-gfbcm -n default --tail=50 -``` - -**关键输出:** -```json -{"level":"info","ts":"2026-02-22T08:01:56Z","msg":"Starting Controller","controller":"redissentinel"} -{"level":"info","ts":"2026-02-22T08:01:56Z","msg":"Starting workers","controller":"redissentinel","worker count":1} -``` - -**分析:** -- ✅ RedisSentinel Controller 已启动 -- ✅ 没有错误日志 -- ❌ 但也没有处理 user-redis 资源的日志 -- **推测:** Operator 在等待某个依赖资源 - ---- - -### 步骤 8:检查 RedisReplication 资源(关键发现) - -**目的:** 根据 RedisSentinel 配置中的 `redisReplicationName: user-redis`,检查对应的 RedisReplication 是否存在 - -**命令:** -```bash -kubectl get redisreplication -n juwan -``` - -**输出:** -``` -No resources found in juwan namespace. -``` - -**分析:** -- ❌ **RedisReplication 资源不存在!** -- 🔎 **这就是问题的根本原因** - ---- - -## 💡 根因分析 - -### 问题根源 - -**RedisSentinel 依赖 RedisReplication,但配置中只创建了 RedisSentinel,没有创建 RedisReplication。** - -### Redis Operator 架构理解 - -在 OpsTree Redis Operator 中,资源之间的关系如下: - -``` -┌─────────────────────────────────────────┐ -│ RedisSentinel (哨兵层) │ -│ - 3个 Sentinel 节点 │ -│ - 负责监控和自动故障转移 │ -│ - 引用: redisReplicationName │ -└──────────────┬──────────────────────────┘ - │ 监控 - ↓ -┌─────────────────────────────────────────┐ -│ RedisReplication (数据层) │ -│ - 1个 Master + N个 Replica │ -│ - 提供实际的缓存服务 │ -│ - 主从复制 │ -└─────────────────────────────────────────┘ -``` - -### 错误配置的问题 - -原始配置直接创建了 RedisSentinel,但: - -1. **缺少被监控对象:** Sentinel 需要监控一个 RedisReplication 集群 -2. **引用不存在的资源:** `redisReplicationName: user-redis` 指向一个不存在的 RedisReplication -3. **Operator 行为:** Operator 发现依赖的 RedisReplication 不存在,因此不会创建 Sentinel Pod - -### 为什么没有错误提示? - -- CRD 验证只检查语法和字段类型 -- 资源引用关系由 Operator 运行时检查 -- Operator 采用了"等待依赖"策略,而不是报错 - ---- - -## ✅ 解决方案 - -### 正确的部署顺序 - -1. **先创建 RedisReplication**(建立 Redis 主从复制集群) -2. **再创建 RedisSentinel**(监控上述复制集群) - -### 配置结构 - -```yaml -# 第一步:创建 Redis 主从复制(数据层) -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisReplication -metadata: - name: user-redis # Sentinel 将引用这个名称 - namespace: juwan -spec: - clusterSize: 3 # 1 Master + 2 Replicas - kubernetesConfig: - image: quay.io/opstree/redis:v7.0.12 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - redisSecret: - name: user-redis - key: password - storage: - volumeClaimTemplate: - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi # 每个 Redis 节点 1GB 存储 - ---- -# 第二步:创建 Sentinel 监控(监控层) -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisSentinel -metadata: - name: user-redis-sentinel # 使用不同的名称避免混淆 - namespace: juwan -spec: - clusterSize: 3 # 3个 Sentinel 节点(推荐奇数) - kubernetesConfig: - image: quay.io/opstree/redis-sentinel:v7.0.12 # 使用 Sentinel 专用镜像 - redisSentinelConfig: - redisReplicationName: user-redis # 引用上面的 RedisReplication - masterGroupName: mymaster - quorum: "2" # 需要 2 个 Sentinel 同意才能进行故障转移 -``` - ---- - -## 🔧 修复步骤 - -### 步骤 1:删除错误的 RedisSentinel 资源 - -**命令:** -```bash -kubectl delete redissentinel user-redis -n juwan -``` - -**输出:** -``` -redissentinel.redis.redis.opstreelabs.in "user-redis" deleted -``` - -**说明:** 删除仅创建了 CRD 实例但未创建实际 Pod 的资源 - ---- - -### 步骤 2:更新配置文件 - -修改 `deploy/k8s/service/user/user-rpc.yaml`,将单独的 RedisSentinel 替换为: -1. RedisReplication(数据层) -2. RedisSentinel(监控层) - -**变更内容:** -- 添加 `RedisReplication` 资源定义 -- 添加 `storage.volumeClaimTemplate` 配置 -- 修改 RedisSentinel 的 `metadata.name` 为 `user-redis-sentinel` -- 使用正确的 Sentinel 镜像:`quay.io/opstree/redis-sentinel:v7.0.12` -- 完善 Sentinel 配置参数 - ---- - -### 步骤 3:应用更新后的配置 - -**命令:** -```bash -kubectl apply -f .\deploy\k8s\service\user\user-rpc.yaml -``` - -**输出:** -``` -deployment.apps/user-rpc configured -service/user-rpc-svc unchanged -horizontalpodautoscaler.autoscaling/user-rpc-hpa-c unchanged -horizontalpodautoscaler.autoscaling/user-rpc-hpa-m unchanged -redisreplication.redis.redis.opstreelabs.in/user-redis created ✅ -redissentinel.redis.redis.opstreelabs.in/user-redis-sentinel created ✅ -cluster.postgresql.cnpg.io/user-db unchanged -``` - -**分析:** -- ✅ RedisReplication 成功创建 -- ✅ RedisSentinel 成功创建 -- 🎯 两个资源都是新创建(created),符合预期 - ---- - -## ✅ 验证结果 - -### 验证 1:检查 Pod 创建情况(等待 30 秒) - -**命令:** -```bash -kubectl get statefulset,pods -n juwan | Select-String -Pattern "user-redis|NAME" -``` - -**输出:** -``` -NAME READY AGE -statefulset.apps/user-redis 3/3 81s ✅ -statefulset.apps/user-redis-sentinel-sentinel 3/3 24s ✅ - -NAME READY STATUS RESTARTS AGE -pod/user-redis-0 2/2 Running 0 80s ✅ -pod/user-redis-1 2/2 Running 0 52s ✅ -pod/user-redis-2 2/2 Running 0 47s ✅ -pod/user-redis-sentinel-sentinel-0 1/1 Running 0 24s ✅ -pod/user-redis-sentinel-sentinel-1 1/1 Running 0 8s ✅ -pod/user-redis-sentinel-sentinel-2 1/1 Running 0 5s ✅ -``` - -**分析:** -- ✅ **RedisReplication** 创建了 3 个 Pod(user-redis-0/1/2) - - 每个 Pod 有 2 个容器(2/2):Redis + Exporter - - 所有 Pod 处于 Running 状态 -- ✅ **RedisSentinel** 创建了 3 个 Pod(user-redis-sentinel-sentinel-0/1/2) - - 每个 Pod 有 1 个容器(1/1):Sentinel - - 所有 Pod 处于 Running 状态 -- ✅ 创建了 2 个 StatefulSet,READY 状态为 3/3 - ---- - -### 验证 2:检查 Service 资源 - -**命令:** -```bash -kubectl get svc -n juwan | Select-String -Pattern "redis|NAME" -``` - -**输出:** -``` -NAME TYPE CLUSTER-IP PORT(S) AGE -user-redis ClusterIP 10.103.91.84 6379/TCP,9121/TCP 95s ✅ -user-redis-additional ClusterIP 10.107.228.48 6379/TCP 95s -user-redis-headless ClusterIP None 6379/TCP 95s ✅ -user-redis-master ClusterIP 10.97.120.76 6379/TCP 95s ✅ -user-redis-replica ClusterIP 10.100.213.103 6379/TCP 95s ✅ -user-redis-sentinel-sentinel ClusterIP 10.105.28.231 26379/TCP 40s ✅ -user-redis-sentinel-sentinel-additional ClusterIP 10.97.111.42 26379/TCP 39s -user-redis-sentinel-sentinel-headless ClusterIP None 26379/TCP 41s -``` - -**Service 功能说明:** - -#### Redis 数据层 Service(端口 6379) -- **user-redis-master**: 主节点服务,用于写操作 -- **user-redis-replica**: 从节点服务,用于读操作 -- **user-redis**: 通用访问入口(负载均衡到所有节点) -- **user-redis-headless**: 无头服务,用于 StatefulSet Pod 间通信 -- **user-redis-additional**: 额外的访问入口 - -#### Sentinel 监控层 Service(端口 26379) -- **user-redis-sentinel-sentinel**: Sentinel 访问入口 -- **user-redis-sentinel-sentinel-headless**: Sentinel 节点间通信 -- **user-redis-sentinel-sentinel-additional**: 额外的 Sentinel 访问入口 - ---- - -### 验证 3:检查完整的集群状态 - -**命令:** -```bash -kubectl get all -n juwan -``` - -**最终状态统计:** - -| 资源类型 | 名称 | 数量 | 状态 | -|---------|------|------|------| -| **Deployment** | user-api | 3/3 | ✅ Running | -| **Deployment** | user-rpc | 3/3 | ✅ Running | -| **StatefulSet** | cluster-example (PostgreSQL) | 3/3 | ✅ Running | -| **StatefulSet** | user-db (PostgreSQL) | 3/3 | ✅ Running | -| **StatefulSet** | user-redis (Redis 数据) | 3/3 | ✅ Running | -| **StatefulSet** | user-redis-sentinel-sentinel | 3/3 | ✅ Running | - -**Pod 总计:** 18 个(全部 Running) -**Service 总计:** 13 个 -**HPA 总计:** 6 个 - ---- - -## 📊 架构图 - -### 部署后的 Redis 架构 - -``` -┌────────────────────────────────────────────────────────────┐ -│ 应用层 (user-rpc) │ -│ │ -│ [需要添加 Redis 连接配置] │ -└──────────┬─────────────────────────────┬───────────────────┘ - │ │ - │ 写操作 │ 读操作 - ↓ ↓ - ┌─────────────┐ ┌─────────────┐ - │ user-redis- │ │ user-redis- │ - │ master │ │ replica │ - │ Service │ │ Service │ - └─────────────┘ └─────────────┘ - │ │ - └──────────┬──────────────────┘ - ↓ - ┌──────────────────────────────────────────┐ - │ RedisReplication (数据层) │ - │ │ - │ ┌──────────┐ ┌──────────┐ ┌───────┐ │ - │ │ Master │→ │ Replica │→ │Replica│ │ - │ │ redis-0 │ │ redis-1 │ │redis-2│ │ - │ └──────────┘ └──────────┘ └───────┘ │ - └──────────────────────────────────────────┘ - ↑ - │ 监控 & 故障转移 - │ - ┌──────────────────────────────────────────┐ - │ RedisSentinel (监控层) │ - │ │ - │ ┌──────────┐ ┌──────────┐ ┌───────┐ │ - │ │Sentinel-0│ │Sentinel-1│ │Sentinel-2│ - │ └──────────┘ └──────────┘ └───────┘ │ - │ │ - │ Quorum: 2/3 (多数派决策) │ - └──────────────────────────────────────────┘ -``` - ---- - -## 📝 后续建议 - -### 1. 应用集成 Redis - -user-rpc 服务目前还没有配置 Redis 连接,需要: - -#### 修改配置文件 `app/users/rpc/etc/pb.yaml` -```yaml -Name: pb.rpc -ListenOn: 0.0.0.0:8080 - -# 添加 Redis 配置(使用 Sentinel 模式) -Redis: - - Host: user-redis-sentinel-sentinel:26379 - Type: sentinel - MasterName: mymaster - Pass: ${REDIS_PASSWORD} - -# 或使用主从模式 -# Redis: -# - Host: user-redis-master:6379 # 写 -# Type: node -# Pass: ${REDIS_PASSWORD} -# - Host: user-redis-replica:6379 # 读 -# Type: node -# Pass: ${REDIS_PASSWORD} - -Etcd: - Hosts: - - etcd-service:2379 # 需要配置实际的 Etcd 地址 - Key: pb.rpc -``` - -#### 修改 Config 结构 `app/users/rpc/internal/config/config.go` -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf // 添加 Redis 配置 -} -``` - -#### 初始化 Redis 客户端 `app/users/rpc/internal/svc/serviceContext.go` -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis // 添加 Redis 客户端 -} - -func NewServiceContext(c config.Config) *ServiceContext { - return &ServiceContext{ - Config: c, - Redis: redis.MustNewRedis(c.Redis), // 初始化 Redis - } -} -``` - -#### 更新 Deployment 环境变量 -```yaml -# deploy/k8s/service/user/user-rpc.yaml -env: - - name: DB_URI - valueFrom: - secretKeyRef: - name: user-db-app - key: uri - - name: REDIS_PASSWORD # 添加 Redis 密码 - valueFrom: - secretKeyRef: - name: user-redis - key: password -``` - ---- - -### 2. Redis 性能监控 - -已启用 Redis Exporter(端口 9121),可以配置 Prometheus 监控: - -```yaml -apiVersion: v1 -kind: ServiceMonitor -metadata: - name: user-redis-metrics - namespace: juwan -spec: - selector: - matchLabels: - app: user-redis - endpoints: - - port: redis-exporter - interval: 30s -``` - -**监控指标:** -- redis_up: 实例状态 -- redis_connected_clients: 连接数 -- redis_memory_used_bytes: 内存使用 -- redis_commands_processed_total: 命令处理数 -- redis_master_repl_offset: 复制偏移量 - ---- - -### 3. 高可用性测试 - -#### 测试主节点故障转移 -```bash -# 1. 查找当前主节点 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster - -# 2. 模拟主节点故障 -kubectl delete pod user-redis-0 -n juwan - -# 3. 观察 Sentinel 的故障转移过程 -kubectl logs -f user-redis-sentinel-sentinel-0 -n juwan - -# 4. 确认新主节点 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster -``` - -#### 预期结果 -- Sentinel 检测到主节点下线(5 秒) -- 2/3 Sentinel 节点达成共识(quorum=2) -- 自动提升一个从节点为主节点 -- 客户端自动重连到新主节点 - ---- - -### 4. 扩展为分片集群(未来) - -当缓存数据量增长需要横向扩展时,可以迁移到 RedisCluster: - -```yaml -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisCluster -metadata: - name: user-redis-cluster - namespace: juwan -spec: - clusterSize: 6 # 3 主 + 3 从 - kubernetesConfig: - image: quay.io/opstree/redis:v7.0.12 - redisLeader: - replicas: 3 - redisFollower: - replicas: 3 - storage: - volumeClaimTemplate: - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 5Gi -``` - -**迁移步骤:** -1. 部署新的 RedisCluster -2. 使用 redis-cli --cluster import 迁移数据 -3. 更新应用配置指向新集群 -4. 下线旧的 Sentinel 集群 - ---- - -### 5. 备份策略 - -Redis Operator 不提供自动备份,建议配置定时任务: - -```bash -# 创建 CronJob 定期执行 BGSAVE -apiVersion: batch/v1 -kind: CronJob -metadata: - name: redis-backup - namespace: juwan -spec: - schedule: "0 2 * * *" # 每天凌晨 2 点 - jobTemplate: - spec: - template: - spec: - containers: - - name: backup - image: redis:7.0.12 - command: - - /bin/sh - - -c - - | - redis-cli -h user-redis-master -a $REDIS_PASSWORD BGSAVE - # 将 /data/dump.rdb 上传到对象存储 - restartPolicy: OnFailure -``` - ---- - -## 📚 总结 - -### 关键经验 - -1. **理解资源依赖关系:** RedisSentinel 依赖 RedisReplication,部署顺序很重要 -2. **资源命名规范:** 使用清晰的名称区分不同层次的资源(如 user-redis 和 user-redis-sentinel) -3. **诊断思路:** - - 从现象(Pod 缺失)→ 资源状态(CRD 存在)→ Operator 日志 → 依赖检查 - - 逐层排查,最终定位到 RedisReplication 缺失 -4. **验证完整性:** 不仅要检查 Pod,还要验证 Service、StatefulSet 等所有相关资源 - -### 文档价值 - -本文档可用于: -- ✅ 团队知识传承 -- ✅ 类似问题的快速排查手册 -- ✅ 新成员的 Redis Operator 学习资料 -- ✅ 事后复盘和经验总结 - ---- - -**最后更新时间:** 2026年2月22日 -**文档状态:** ✅ 问题已解决,Redis 集群运行正常 -**下一步行动:** 配置应用连接 Redis +# Redis Sentinel 部署问题诊断与修复报告 + +**问题日期:** 2026年2月22日 +**命名空间:** juwan +**涉及资源:** user-rpc deployment, RedisSentinel + +--- + +## 📋 目录 + +1. [问题背景](#问题背景) +2. [问题现象](#问题现象) +3. [诊断过程](#诊断过程) +4. [根因分析](#根因分析) +5. [解决方案](#解决方案) +6. [修复步骤](#修复步骤) +7. [验证结果](#验证结果) +8. [后续建议](#后续建议) + +--- + +## 🎯 问题背景 + +### 部署目标 +部署一个简单的三节点 Redis Sentinel 哨兵集群作为缓存服务,供 user-rpc 服务使用。后续如有需要再扩展为分片集群。 + +### 初始配置 +在 `deploy/k8s/service/user/user-rpc.yaml` 中配置了: +- user-rpc Deployment(3副本) +- user-rpc Service +- HPA(CPU和内存) +- **RedisSentinel 资源** +- PostgreSQL Cluster + +--- + +## 🔴 问题现象 + +### 执行的操作 +```bash +kubectl apply -f .\deploy\k8s\service\user\user-rpc.yaml +``` + +### 输出结果 +``` +deployment.apps/user-rpc configured +service/user-rpc-svc unchanged +horizontalpodautoscaler.autoscaling/user-rpc-hpa-c unchanged +horizontalpodautoscaler.autoscaling/user-rpc-hpa-m unchanged +redissentinel.redis.redis.opstreelabs.in/user-redis unchanged +cluster.postgresql.cnpg.io/user-db unchanged +``` + +### 观察到的异常 +查看命名空间资源: +```bash +kubectl get all -n juwan +``` + +**发现:** +- ✅ user-api pods 正常运行 +- ✅ user-rpc pods 正常运行 +- ✅ PostgreSQL clusters 正常运行 +- ❌ **没有任何 Redis 相关的 Pod** +- ❌ **没有 Redis Service** + +--- + +## 🔍 诊断过程 + +### 步骤 1:检查 RedisSentinel 资源状态 + +**目的:** 确认 RedisSentinel 资源是否被成功创建 + +**命令:** +```bash +kubectl get redissentinel user-redis -n juwan +``` + +**输出:** +``` +NAME AGE +user-redis 9m56s +``` + +**分析:** +- ✅ RedisSentinel 资源已创建 +- ❌ 但没有创建任何 Pod +- **结论:** Operator 没有按照 RedisSentinel 规格创建实际资源 + +--- + +### 步骤 2:查看 RedisSentinel 详细信息 + +**目的:** 检查资源的详细配置和事件 + +**命令:** +```bash +kubectl describe redissentinel user-redis -n juwan +``` + +**关键输出:** +```yaml +API Version: redis.redis.opstreelabs.in/v1beta2 +Kind: RedisSentinel +Metadata: + Creation Timestamp: 2026-02-22T12:41:47Z + Finalizers: + redisSentinelFinalizer + Generation: 2 +Spec: + Cluster Size: 3 + Redis Sentinel Config: + Redis Replication Name: user-redis # ⚠️ 关键配置 + Master Group Name: myMaster + Redis Port: 6379 + Quorum: 2 +Events: # ⚠️ 没有任何事件 +``` + +**分析:** +- 配置中引用了 `redisReplicationName: user-redis` +- 没有任何 Events,说明 Operator 可能在等待某些前置条件 +- **需要进一步检查依赖资源** + +--- + +### 步骤 3:检查 Secret 资源 + +**目的:** 确认 RedisSentinel 所需的密码 Secret 是否存在 + +**命令:** +```bash +kubectl get secret user-redis -n juwan +``` + +**输出:** +``` +NAME TYPE DATA AGE +user-redis Opaque 1 5h48m +``` + +**分析:** +- ✅ Secret 存在,排除认证配置问题 + +--- + +### 步骤 4:查找 Redis Pods + +**目的:** 确认是否有 Redis Pod 使用不同的命名规则 + +**命令:** +```bash +kubectl get pods -n juwan | Select-String redis +``` + +**输出:** +``` +(空输出 - 没有找到任何 Redis Pod) +``` + +**分析:** +- ❌ 确认没有任何 Redis Pod 被创建 + +--- + +### 步骤 5:检查 StatefulSet + +**目的:** Redis 通常使用 StatefulSet 部署,检查是否有相关资源 + +**命令:** +```bash +kubectl get statefulset -n juwan +``` + +**输出:** +``` +No resources found in juwan namespace. +``` + +**分析:** +- ❌ 没有 StatefulSet 被创建 +- **结论:** Operator 完全没有执行创建动作 + +--- + +### 步骤 6:检查 Redis Operator 状态 + +**目的:** 确认 Redis Operator 本身是否正常运行 + +**命令:** +```bash +kubectl get pods -A | Select-String redis-operator +``` + +**输出:** +``` +default redis-operator-7dcf4468c9-gfbcm 1/1 Running 0 4h50m +ot-operators redis-operator-69bb645f5b-6kxkv 0/1 ErrImageNeverPull 0 4h48m +``` + +**分析:** +- ✅ default namespace 中的 Operator 正常运行 +- ⚠️ ot-operators namespace 中的 Operator 镜像拉取失败(但不影响我们的场景) + +--- + +### 步骤 7:查看 Operator 日志 + +**目的:** 从 Operator 日志中寻找线索 + +**命令:** +```bash +kubectl logs redis-operator-7dcf4468c9-gfbcm -n default --tail=50 +``` + +**关键输出:** +```json +{"level":"info","ts":"2026-02-22T08:01:56Z","msg":"Starting Controller","controller":"redissentinel"} +{"level":"info","ts":"2026-02-22T08:01:56Z","msg":"Starting workers","controller":"redissentinel","worker count":1} +``` + +**分析:** +- ✅ RedisSentinel Controller 已启动 +- ✅ 没有错误日志 +- ❌ 但也没有处理 user-redis 资源的日志 +- **推测:** Operator 在等待某个依赖资源 + +--- + +### 步骤 8:检查 RedisReplication 资源(关键发现) + +**目的:** 根据 RedisSentinel 配置中的 `redisReplicationName: user-redis`,检查对应的 RedisReplication 是否存在 + +**命令:** +```bash +kubectl get redisreplication -n juwan +``` + +**输出:** +``` +No resources found in juwan namespace. +``` + +**分析:** +- ❌ **RedisReplication 资源不存在!** +- 🔎 **这就是问题的根本原因** + +--- + +## 💡 根因分析 + +### 问题根源 + +**RedisSentinel 依赖 RedisReplication,但配置中只创建了 RedisSentinel,没有创建 RedisReplication。** + +### Redis Operator 架构理解 + +在 OpsTree Redis Operator 中,资源之间的关系如下: + +``` +┌─────────────────────────────────────────┐ +│ RedisSentinel (哨兵层) │ +│ - 3个 Sentinel 节点 │ +│ - 负责监控和自动故障转移 │ +│ - 引用: redisReplicationName │ +└──────────────┬──────────────────────────┘ + │ 监控 + ↓ +┌─────────────────────────────────────────┐ +│ RedisReplication (数据层) │ +│ - 1个 Master + N个 Replica │ +│ - 提供实际的缓存服务 │ +│ - 主从复制 │ +└─────────────────────────────────────────┘ +``` + +### 错误配置的问题 + +原始配置直接创建了 RedisSentinel,但: + +1. **缺少被监控对象:** Sentinel 需要监控一个 RedisReplication 集群 +2. **引用不存在的资源:** `redisReplicationName: user-redis` 指向一个不存在的 RedisReplication +3. **Operator 行为:** Operator 发现依赖的 RedisReplication 不存在,因此不会创建 Sentinel Pod + +### 为什么没有错误提示? + +- CRD 验证只检查语法和字段类型 +- 资源引用关系由 Operator 运行时检查 +- Operator 采用了"等待依赖"策略,而不是报错 + +--- + +## ✅ 解决方案 + +### 正确的部署顺序 + +1. **先创建 RedisReplication**(建立 Redis 主从复制集群) +2. **再创建 RedisSentinel**(监控上述复制集群) + +### 配置结构 + +```yaml +# 第一步:创建 Redis 主从复制(数据层) +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisReplication +metadata: + name: user-redis # Sentinel 将引用这个名称 + namespace: juwan +spec: + clusterSize: 3 # 1 Master + 2 Replicas + kubernetesConfig: + image: quay.io/opstree/redis:v7.0.12 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + redisSecret: + name: user-redis + key: password + storage: + volumeClaimTemplate: + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi # 每个 Redis 节点 1GB 存储 + +--- +# 第二步:创建 Sentinel 监控(监控层) +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisSentinel +metadata: + name: user-redis-sentinel # 使用不同的名称避免混淆 + namespace: juwan +spec: + clusterSize: 3 # 3个 Sentinel 节点(推荐奇数) + kubernetesConfig: + image: quay.io/opstree/redis-sentinel:v7.0.12 # 使用 Sentinel 专用镜像 + redisSentinelConfig: + redisReplicationName: user-redis # 引用上面的 RedisReplication + masterGroupName: mymaster + quorum: "2" # 需要 2 个 Sentinel 同意才能进行故障转移 +``` + +--- + +## 🔧 修复步骤 + +### 步骤 1:删除错误的 RedisSentinel 资源 + +**命令:** +```bash +kubectl delete redissentinel user-redis -n juwan +``` + +**输出:** +``` +redissentinel.redis.redis.opstreelabs.in "user-redis" deleted +``` + +**说明:** 删除仅创建了 CRD 实例但未创建实际 Pod 的资源 + +--- + +### 步骤 2:更新配置文件 + +修改 `deploy/k8s/service/user/user-rpc.yaml`,将单独的 RedisSentinel 替换为: +1. RedisReplication(数据层) +2. RedisSentinel(监控层) + +**变更内容:** +- 添加 `RedisReplication` 资源定义 +- 添加 `storage.volumeClaimTemplate` 配置 +- 修改 RedisSentinel 的 `metadata.name` 为 `user-redis-sentinel` +- 使用正确的 Sentinel 镜像:`quay.io/opstree/redis-sentinel:v7.0.12` +- 完善 Sentinel 配置参数 + +--- + +### 步骤 3:应用更新后的配置 + +**命令:** +```bash +kubectl apply -f .\deploy\k8s\service\user\user-rpc.yaml +``` + +**输出:** +``` +deployment.apps/user-rpc configured +service/user-rpc-svc unchanged +horizontalpodautoscaler.autoscaling/user-rpc-hpa-c unchanged +horizontalpodautoscaler.autoscaling/user-rpc-hpa-m unchanged +redisreplication.redis.redis.opstreelabs.in/user-redis created ✅ +redissentinel.redis.redis.opstreelabs.in/user-redis-sentinel created ✅ +cluster.postgresql.cnpg.io/user-db unchanged +``` + +**分析:** +- ✅ RedisReplication 成功创建 +- ✅ RedisSentinel 成功创建 +- 🎯 两个资源都是新创建(created),符合预期 + +--- + +## ✅ 验证结果 + +### 验证 1:检查 Pod 创建情况(等待 30 秒) + +**命令:** +```bash +kubectl get statefulset,pods -n juwan | Select-String -Pattern "user-redis|NAME" +``` + +**输出:** +``` +NAME READY AGE +statefulset.apps/user-redis 3/3 81s ✅ +statefulset.apps/user-redis-sentinel-sentinel 3/3 24s ✅ + +NAME READY STATUS RESTARTS AGE +pod/user-redis-0 2/2 Running 0 80s ✅ +pod/user-redis-1 2/2 Running 0 52s ✅ +pod/user-redis-2 2/2 Running 0 47s ✅ +pod/user-redis-sentinel-sentinel-0 1/1 Running 0 24s ✅ +pod/user-redis-sentinel-sentinel-1 1/1 Running 0 8s ✅ +pod/user-redis-sentinel-sentinel-2 1/1 Running 0 5s ✅ +``` + +**分析:** +- ✅ **RedisReplication** 创建了 3 个 Pod(user-redis-0/1/2) + - 每个 Pod 有 2 个容器(2/2):Redis + Exporter + - 所有 Pod 处于 Running 状态 +- ✅ **RedisSentinel** 创建了 3 个 Pod(user-redis-sentinel-sentinel-0/1/2) + - 每个 Pod 有 1 个容器(1/1):Sentinel + - 所有 Pod 处于 Running 状态 +- ✅ 创建了 2 个 StatefulSet,READY 状态为 3/3 + +--- + +### 验证 2:检查 Service 资源 + +**命令:** +```bash +kubectl get svc -n juwan | Select-String -Pattern "redis|NAME" +``` + +**输出:** +``` +NAME TYPE CLUSTER-IP PORT(S) AGE +user-redis ClusterIP 10.103.91.84 6379/TCP,9121/TCP 95s ✅ +user-redis-additional ClusterIP 10.107.228.48 6379/TCP 95s +user-redis-headless ClusterIP None 6379/TCP 95s ✅ +user-redis-master ClusterIP 10.97.120.76 6379/TCP 95s ✅ +user-redis-replica ClusterIP 10.100.213.103 6379/TCP 95s ✅ +user-redis-sentinel-sentinel ClusterIP 10.105.28.231 26379/TCP 40s ✅ +user-redis-sentinel-sentinel-additional ClusterIP 10.97.111.42 26379/TCP 39s +user-redis-sentinel-sentinel-headless ClusterIP None 26379/TCP 41s +``` + +**Service 功能说明:** + +#### Redis 数据层 Service(端口 6379) +- **user-redis-master**: 主节点服务,用于写操作 +- **user-redis-replica**: 从节点服务,用于读操作 +- **user-redis**: 通用访问入口(负载均衡到所有节点) +- **user-redis-headless**: 无头服务,用于 StatefulSet Pod 间通信 +- **user-redis-additional**: 额外的访问入口 + +#### Sentinel 监控层 Service(端口 26379) +- **user-redis-sentinel-sentinel**: Sentinel 访问入口 +- **user-redis-sentinel-sentinel-headless**: Sentinel 节点间通信 +- **user-redis-sentinel-sentinel-additional**: 额外的 Sentinel 访问入口 + +--- + +### 验证 3:检查完整的集群状态 + +**命令:** +```bash +kubectl get all -n juwan +``` + +**最终状态统计:** + +| 资源类型 | 名称 | 数量 | 状态 | +|---------|------|------|------| +| **Deployment** | user-api | 3/3 | ✅ Running | +| **Deployment** | user-rpc | 3/3 | ✅ Running | +| **StatefulSet** | cluster-example (PostgreSQL) | 3/3 | ✅ Running | +| **StatefulSet** | user-db (PostgreSQL) | 3/3 | ✅ Running | +| **StatefulSet** | user-redis (Redis 数据) | 3/3 | ✅ Running | +| **StatefulSet** | user-redis-sentinel-sentinel | 3/3 | ✅ Running | + +**Pod 总计:** 18 个(全部 Running) +**Service 总计:** 13 个 +**HPA 总计:** 6 个 + +--- + +## 📊 架构图 + +### 部署后的 Redis 架构 + +``` +┌────────────────────────────────────────────────────────────┐ +│ 应用层 (user-rpc) │ +│ │ +│ [需要添加 Redis 连接配置] │ +└──────────┬─────────────────────────────┬───────────────────┘ + │ │ + │ 写操作 │ 读操作 + ↓ ↓ + ┌─────────────┐ ┌─────────────┐ + │ user-redis- │ │ user-redis- │ + │ master │ │ replica │ + │ Service │ │ Service │ + └─────────────┘ └─────────────┘ + │ │ + └──────────┬──────────────────┘ + ↓ + ┌──────────────────────────────────────────┐ + │ RedisReplication (数据层) │ + │ │ + │ ┌──────────┐ ┌──────────┐ ┌───────┐ │ + │ │ Master │→ │ Replica │→ │Replica│ │ + │ │ redis-0 │ │ redis-1 │ │redis-2│ │ + │ └──────────┘ └──────────┘ └───────┘ │ + └──────────────────────────────────────────┘ + ↑ + │ 监控 & 故障转移 + │ + ┌──────────────────────────────────────────┐ + │ RedisSentinel (监控层) │ + │ │ + │ ┌──────────┐ ┌──────────┐ ┌───────┐ │ + │ │Sentinel-0│ │Sentinel-1│ │Sentinel-2│ + │ └──────────┘ └──────────┘ └───────┘ │ + │ │ + │ Quorum: 2/3 (多数派决策) │ + └──────────────────────────────────────────┘ +``` + +--- + +## 📝 后续建议 + +### 1. 应用集成 Redis + +user-rpc 服务目前还没有配置 Redis 连接,需要: + +#### 修改配置文件 `app/users/rpc/etc/pb.yaml` +```yaml +Name: pb.rpc +ListenOn: 0.0.0.0:8080 + +# 添加 Redis 配置(使用 Sentinel 模式) +Redis: + - Host: user-redis-sentinel-sentinel:26379 + Type: sentinel + MasterName: mymaster + Pass: ${REDIS_PASSWORD} + +# 或使用主从模式 +# Redis: +# - Host: user-redis-master:6379 # 写 +# Type: node +# Pass: ${REDIS_PASSWORD} +# - Host: user-redis-replica:6379 # 读 +# Type: node +# Pass: ${REDIS_PASSWORD} + +Etcd: + Hosts: + - etcd-service:2379 # 需要配置实际的 Etcd 地址 + Key: pb.rpc +``` + +#### 修改 Config 结构 `app/users/rpc/internal/config/config.go` +```go +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + Redis redis.RedisConf // 添加 Redis 配置 +} +``` + +#### 初始化 Redis 客户端 `app/users/rpc/internal/svc/serviceContext.go` +```go +package svc + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "juwan-backend/app/users/rpc/internal/config" +) + +type ServiceContext struct { + Config config.Config + Redis *redis.Redis // 添加 Redis 客户端 +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + Redis: redis.MustNewRedis(c.Redis), // 初始化 Redis + } +} +``` + +#### 更新 Deployment 环境变量 +```yaml +# deploy/k8s/service/user/user-rpc.yaml +env: + - name: DB_URI + valueFrom: + secretKeyRef: + name: user-db-app + key: uri + - name: REDIS_PASSWORD # 添加 Redis 密码 + valueFrom: + secretKeyRef: + name: user-redis + key: password +``` + +--- + +### 2. Redis 性能监控 + +已启用 Redis Exporter(端口 9121),可以配置 Prometheus 监控: + +```yaml +apiVersion: v1 +kind: ServiceMonitor +metadata: + name: user-redis-metrics + namespace: juwan +spec: + selector: + matchLabels: + app: user-redis + endpoints: + - port: redis-exporter + interval: 30s +``` + +**监控指标:** +- redis_up: 实例状态 +- redis_connected_clients: 连接数 +- redis_memory_used_bytes: 内存使用 +- redis_commands_processed_total: 命令处理数 +- redis_master_repl_offset: 复制偏移量 + +--- + +### 3. 高可用性测试 + +#### 测试主节点故障转移 +```bash +# 1. 查找当前主节点 +kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster + +# 2. 模拟主节点故障 +kubectl delete pod user-redis-0 -n juwan + +# 3. 观察 Sentinel 的故障转移过程 +kubectl logs -f user-redis-sentinel-sentinel-0 -n juwan + +# 4. 确认新主节点 +kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster +``` + +#### 预期结果 +- Sentinel 检测到主节点下线(5 秒) +- 2/3 Sentinel 节点达成共识(quorum=2) +- 自动提升一个从节点为主节点 +- 客户端自动重连到新主节点 + +--- + +### 4. 扩展为分片集群(未来) + +当缓存数据量增长需要横向扩展时,可以迁移到 RedisCluster: + +```yaml +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisCluster +metadata: + name: user-redis-cluster + namespace: juwan +spec: + clusterSize: 6 # 3 主 + 3 从 + kubernetesConfig: + image: quay.io/opstree/redis:v7.0.12 + redisLeader: + replicas: 3 + redisFollower: + replicas: 3 + storage: + volumeClaimTemplate: + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi +``` + +**迁移步骤:** +1. 部署新的 RedisCluster +2. 使用 redis-cli --cluster import 迁移数据 +3. 更新应用配置指向新集群 +4. 下线旧的 Sentinel 集群 + +--- + +### 5. 备份策略 + +Redis Operator 不提供自动备份,建议配置定时任务: + +```bash +# 创建 CronJob 定期执行 BGSAVE +apiVersion: batch/v1 +kind: CronJob +metadata: + name: redis-backup + namespace: juwan +spec: + schedule: "0 2 * * *" # 每天凌晨 2 点 + jobTemplate: + spec: + template: + spec: + containers: + - name: backup + image: redis:7.0.12 + command: + - /bin/sh + - -c + - | + redis-cli -h user-redis-master -a $REDIS_PASSWORD BGSAVE + # 将 /data/dump.rdb 上传到对象存储 + restartPolicy: OnFailure +``` + +--- + +## 📚 总结 + +### 关键经验 + +1. **理解资源依赖关系:** RedisSentinel 依赖 RedisReplication,部署顺序很重要 +2. **资源命名规范:** 使用清晰的名称区分不同层次的资源(如 user-redis 和 user-redis-sentinel) +3. **诊断思路:** + - 从现象(Pod 缺失)→ 资源状态(CRD 存在)→ Operator 日志 → 依赖检查 + - 逐层排查,最终定位到 RedisReplication 缺失 +4. **验证完整性:** 不仅要检查 Pod,还要验证 Service、StatefulSet 等所有相关资源 + +### 文档价值 + +本文档可用于: +- ✅ 团队知识传承 +- ✅ 类似问题的快速排查手册 +- ✅ 新成员的 Redis Operator 学习资料 +- ✅ 事后复盘和经验总结 + +--- + +**最后更新时间:** 2026年2月22日 +**文档状态:** ✅ 问题已解决,Redis 集群运行正常 +**下一步行动:** 配置应用连接 Redis diff --git a/docs/redis-services-guide.md b/docs/redis-services-guide.md index 22cbcca..7dbe018 100644 --- a/docs/redis-services-guide.md +++ b/docs/redis-services-guide.md @@ -1,1179 +1,1179 @@ -# Redis Services 连接指南 - -**环境:** juwan namespace -**Redis 版本:** 7.0.12 -**部署模式:** RedisReplication + RedisSentinel -**文档日期:** 2026年2月22日 - ---- - -## 📋 目录 - -1. [Service 列表总览](#service-列表总览) -2. [Redis 数据层 Service 详解](#redis-数据层-service-详解) -3. [Sentinel 监控层 Service 详解](#sentinel-监控层-service-详解) -4. [管理工具连接方式](#管理工具连接方式) -5. [应用服务连接方式](#应用服务连接方式) -6. [连接方式对比](#连接方式对比) -7. [最佳实践建议](#最佳实践建议) -8. [故障排查](#故障排查) - ---- - -## 📊 Service 列表总览 - -当前集群中部署的 Redis 相关 Service: - -| Service 名称 | 类型 | Cluster IP | 端口 | 用途 | -|-------------|------|-----------|------|------| -| **user-redis** | ClusterIP | 10.103.91.84 | 6379, 9121 | 通用访问 + 监控 | -| **user-redis-additional** | ClusterIP | 10.107.228.48 | 6379 | 额外访问入口 | -| **user-redis-headless** | ClusterIP(None) | None | 6379 | Pod 间直接访问 | -| **user-redis-master** ⭐ | ClusterIP | 10.97.120.76 | 6379 | 主节点访问 | -| **user-redis-replica** ⭐ | ClusterIP | 10.100.213.103 | 6379 | 从节点访问 | -| **user-redis-sentinel-sentinel** ⭐⭐⭐ | ClusterIP | 10.105.28.231 | 26379 | Sentinel 访问 | -| **user-redis-sentinel-sentinel-additional** | ClusterIP | 10.97.111.42 | 26379 | 额外 Sentinel 入口 | -| **user-redis-sentinel-sentinel-headless** | ClusterIP(None) | None | 26379 | Sentinel 间通信 | - -**图例:** -- ⭐⭐⭐ 生产环境强烈推荐 -- ⭐ 生产环境可用 -- 无标记:特殊场景或内部使用 - ---- - -## 🔴 Redis 数据层 Service 详解 - -### 1. user-redis-master ⭐ - -**基本信息** -```yaml -名称: user-redis-master -类型: ClusterIP -IP: 10.97.120.76 -端口: 6379/TCP -DNS: user-redis-master.juwan.svc.cluster.local -``` - -**功能特点** -- 🎯 自动追踪当前 Redis 主节点 -- ✅ 确保所有写操作到达主节点 -- 🔄 故障转移后自动指向新主节点 -- 💪 提供最强一致性保证 - -**适用场景** -- ✅ 所有写操作(SET, HSET, ZADD 等) -- ✅ 需要强一致性的读操作 -- ✅ 事务操作(MULTI/EXEC) -- ❌ 不适合高并发读请求 - -**连接示例** -```go -// Go - go-redis -rdb := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, -}) - -// 写操作 -err := rdb.Set(ctx, "user:1001", "John Doe", 0).Err() -``` - -```bash -# CLI 测试 -kubectl run -it --rm redis-cli --image=redis:7.0.12 --restart=Never -n juwan -- \ - redis-cli -h user-redis-master -a SET test "hello" -``` - ---- - -### 2. user-redis-replica ⭐ - -**基本信息** -```yaml -名称: user-redis-replica -类型: ClusterIP -IP: 10.100.213.103 -端口: 6379/TCP -DNS: user-redis-replica.juwan.svc.cluster.local -``` - -**功能特点** -- 📖 负载均衡到所有从节点(当前 2 个) -- ⚡ 分散读请求,提升吞吐量 -- 🕐 可能存在轻微的复制延迟(通常 < 100ms) -- 🚫 只读模式,写操作会失败 - -**适用场景** -- ✅ 高并发读请求 -- ✅ 查询操作(GET, HGET, ZRANGE 等) -- ✅ 统计分析类查询 -- ⚠️ 对数据实时性要求不高的场景 -- ❌ 不能用于写操作 - -**连接示例** -```go -// Go - 读写分离配置 -masterClient := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Password: os.Getenv("REDIS_PASSWORD"), -}) - -replicaClient := redis.NewClient(&redis.Options{ - Addr: "user-redis-replica.juwan.svc.cluster.local:6379", - Password: os.Getenv("REDIS_PASSWORD"), - ReadOnly: true, -}) - -// 写操作用 master -masterClient.Set(ctx, "counter", 100, 0) - -// 读操作用 replica -val, err := replicaClient.Get(ctx, "counter").Result() -``` - ---- - -### 3. user-redis - -**基本信息** -```yaml -名称: user-redis -类型: ClusterIP -IP: 10.103.91.84 -端口: 6379/TCP (Redis), 9121/TCP (Exporter) -DNS: user-redis.juwan.svc.cluster.local -``` - -**功能特点** -- 🔀 负载均衡到所有 Redis 节点(主 + 从) -- 📊 端口 9121 暴露 Prometheus 指标 -- ⚠️ 写操作可能路由到从节点导致失败 - -**适用场景** -- ✅ Prometheus 监控抓取(端口 9121) -- ⚠️ 测试环境的简单访问 -- ❌ 不推荐生产环境读写操作 - -**监控配置** -```yaml -# Prometheus ServiceMonitor -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: redis-metrics - namespace: juwan -spec: - selector: - matchLabels: - app: redis - endpoints: - - port: redis-exporter - interval: 30s - path: /metrics -``` - ---- - -### 4. user-redis-additional - -**基本信息** -```yaml -名称: user-redis-additional -类型: ClusterIP -IP: 10.107.228.48 -端口: 6379/TCP -``` - -**功能特点** -- 功能类似 user-redis -- 提供额外的访问入口 -- 用于多租户或网络隔离场景 - -**适用场景** -- 特殊网络策略场景 -- 多应用隔离访问 -- 备用访问点 - ---- - -### 5. user-redis-headless - -**基本信息** -```yaml -名称: user-redis-headless -类型: ClusterIP (Headless - None) -端口: 6379/TCP -DNS: - - user-redis-0.user-redis-headless.juwan.svc.cluster.local - - user-redis-1.user-redis-headless.juwan.svc.cluster.local - - user-redis-2.user-redis-headless.juwan.svc.cluster.local -``` - -**功能特点** -- 🎯 直接返回所有 Pod IP,不做负载均衡 -- 🔗 用于 StatefulSet Pod 间通信 -- 📡 Redis 主从复制使用此服务发现 - -**适用场景** -- ✅ 内部复制通信 -- ✅ 集群管理操作 -- ✅ 需要直接访问特定 Pod -- ❌ 不适合应用层使用 - -**直接访问示例** -```bash -# 直接连接 user-redis-0 -redis-cli -h user-redis-0.user-redis-headless.juwan.svc.cluster.local -a - -# DNS 解析会返回具体 Pod IP -nslookup user-redis-headless.juwan.svc.cluster.local -``` - ---- - -## 🟡 Sentinel 监控层 Service 详解 - -### 1. user-redis-sentinel-sentinel ⭐⭐⭐ - -**基本信息** -```yaml -名称: user-redis-sentinel-sentinel -类型: ClusterIP -IP: 10.105.28.231 -端口: 26379/TCP -DNS: user-redis-sentinel-sentinel.juwan.svc.cluster.local -``` - -**功能特点** -- 🛡️ 提供高可用 Redis 访问 -- 🔄 自动发现主节点 -- ⚡ 主节点故障时自动切换 -- 📍 客户端自动跟踪主节点变化 - -**Sentinel 架构** -``` -应用 → Sentinel Service → Sentinel 节点 (3个) - ↓ - 监控 Redis 集群 - ↓ - 自动发现当前主节点位置 -``` - -**适用场景** -- ✅✅✅ **生产环境强烈推荐** -- ✅ 需要自动故障转移 -- ✅ 高可用架构 -- ✅ 无需手动处理主从切换 - -**连接示例** -```go -// Go - go-redis Sentinel 模式 -rdb := redis.NewFailoverClient(&redis.FailoverOptions{ - MasterName: "mymaster", - SentinelAddrs: []string{ - "user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379", - }, - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, - - // 连接池配置 - PoolSize: 10, - MinIdleConns: 5, - - // 超时配置 - DialTimeout: 5 * time.Second, - ReadTimeout: 3 * time.Second, - WriteTimeout: 3 * time.Second, -}) - -// 使用方式与普通客户端完全一致 -err := rdb.Set(ctx, "key", "value", 0).Err() -val, err := rdb.Get(ctx, "key").Result() -``` - -```python -# Python - redis-py Sentinel 模式 -from redis.sentinel import Sentinel - -sentinel = Sentinel([ - ('user-redis-sentinel-sentinel.juwan.svc.cluster.local', 26379) -], socket_timeout=0.5) - -# 获取主节点(写) -master = sentinel.master_for('mymaster', - password=os.getenv('REDIS_PASSWORD'), - socket_timeout=0.5) -master.set('key', 'value') - -# 获取从节点(读) -slave = sentinel.slave_for('mymaster', - password=os.getenv('REDIS_PASSWORD'), - socket_timeout=0.5) -value = slave.get('key') -``` - -```yaml -# Spring Boot application.yml -spring: - redis: - sentinel: - master: mymaster - nodes: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - password: ${REDIS_PASSWORD} -``` - -**Sentinel 命令查询** -```bash -# 查看主节点信息 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster - -# 查看所有监控的主节点 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL masters - -# 查看从节点列表 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL slaves mymaster - -# 查看 Sentinel 节点 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL sentinels mymaster -``` - ---- - -### 2. user-redis-sentinel-sentinel-additional - -**基本信息** -```yaml -名称: user-redis-sentinel-sentinel-additional -类型: ClusterIP -IP: 10.97.111.42 -端口: 26379/TCP -``` - -**功能特点** -- 功能同 user-redis-sentinel-sentinel -- 提供额外访问入口 -- 用于多客户端分离 - -**适用场景** -- 多应用共享 Redis 时的访问隔离 -- 网络策略要求 -- 流量分离 - ---- - -### 3. user-redis-sentinel-sentinel-headless - -**基本信息** -```yaml -名称: user-redis-sentinel-sentinel-headless -类型: ClusterIP (Headless - None) -端口: 26379/TCP -``` - -**功能特点** -- Sentinel 节点间通信 -- 选举和投票 -- 状态同步 - -**适用场景** -- 内部使用,应用层无需关注 - ---- - -## 🔧 管理工具连接方式 - -### 使用 kubectl port-forward(推荐) - -#### 方式一:连接主节点 -```bash -# 转发主节点服务到本地 -kubectl port-forward -n juwan svc/user-redis-master 6379:6379 - -# 或直接转发到 Pod -kubectl port-forward -n juwan pod/user-redis-0 6379:6379 -``` - -然后在管理工具中配置: -- **Host**: localhost -- **Port**: 6379 -- **Password**: (见下方获取方法) - -#### 方式二:使用 LoadBalancer(生产不推荐) -```yaml -# 临时暴露服务(仅用于调试) -apiVersion: v1 -kind: Service -metadata: - name: redis-external - namespace: juwan -spec: - type: LoadBalancer - selector: - app: user-redis - ports: - - port: 6379 - targetPort: 6379 -``` - ---- - -### 获取 Redis 密码 - -```bash -# 方式一:直接输出 -kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d - -# 方式二:设置为环境变量 -export REDIS_PASSWORD=$(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) -echo $REDIS_PASSWORD - -# 方式三:保存到文件 -kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d > redis-password.txt -``` - ---- - -### 常用管理工具配置 - -#### Redis Desktop Manager (RedisInsight) -``` -Connection Name: juwan-user-redis -Host: localhost (使用 port-forward) -Port: 6379 -Username: (留空) -Password: (从 Secret 获取) -``` - -#### redis-cli -```bash -# 在集群内访问 -kubectl exec -it user-redis-0 -n juwan -- redis-cli -a - -# 从本地访问(需要 port-forward) -redis-cli -h localhost -p 6379 -a - -# 常用命令 -INFO replication # 查看复制状态 -INFO stats # 查看统计信息 -CLUSTER INFO # 查看集群信息 -KEYS * # 查看所有 key(生产谨慎使用) -``` - ---- - -## 💻 应用服务连接方式 - -### 方案一:Sentinel 模式(生产强烈推荐)⭐⭐⭐ - -**优点** -- ✅ 自动故障转移 -- ✅ 高可用 -- ✅ 无需手动切换 -- ✅ 客户端自动重连 -- ✅ 支持读写分离 - -#### Go-Zero 配置 - -**配置文件** `app/users/rpc/etc/pb.yaml` -```yaml -Name: pb.rpc -ListenOn: 0.0.0.0:9001 - -# Redis Sentinel 配置 -Redis: - Type: sentinel - MasterName: mymaster - SentinelAddrs: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Pass: ${REDIS_PASSWORD} # 从环境变量读取 - -Etcd: - Hosts: - - etcd-service.juwan.svc.cluster.local:2379 - Key: pb.rpc -``` - -**Config 结构** `app/users/rpc/internal/config/config.go` -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf -} -``` - -**初始化 Redis** `app/users/rpc/internal/svc/serviceContext.go` -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis -} - -func NewServiceContext(c config.Config) *ServiceContext { - return &ServiceContext{ - Config: c, - Redis: redis.MustNewRedis(c.Redis), - } -} -``` - -**使用示例** `app/users/rpc/internal/logic/getUsersByIdLogic.go` -```go -func (l *GetUsersByIdLogic) GetUsersById(in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { - // 尝试从缓存获取 - cacheKey := fmt.Sprintf("user:%d", in.Id) - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - // 缓存命中 - var user pb.User - json.Unmarshal([]byte(cached), &user) - return &pb.GetUsersByIdResp{User: &user}, nil - } - - // 从数据库查询 - user := l.fetchUserFromDB(in.Id) - - // 写入缓存 - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) // 1小时过期 - - return &pb.GetUsersByIdResp{User: user}, nil -} -``` - -#### Go (原生 go-redis) - -```go -package main - -import ( - "context" - "github.com/redis/go-redis/v9" - "time" -) - -func NewRedisClient() *redis.Client { - rdb := redis.NewFailoverClient(&redis.FailoverOptions{ - MasterName: "mymaster", - SentinelAddrs: []string{ - "user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379", - }, - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, - - // 连接池配置 - PoolSize: 10, - MinIdleConns: 5, - - // 超时配置 - DialTimeout: 5 * time.Second, - ReadTimeout: 3 * time.Second, - WriteTimeout: 3 * time.Second, - - // 重试配置 - MaxRetries: 3, - MinRetryBackoff: 8 * time.Millisecond, - MaxRetryBackoff: 512 * time.Millisecond, - }) - - // 测试连接 - ctx := context.Background() - if err := rdb.Ping(ctx).Err(); err != nil { - panic(err) - } - - return rdb -} -``` - -#### Python (redis-py) - -```python -from redis.sentinel import Sentinel -import os - -# 初始化 Sentinel -sentinel = Sentinel([ - ('user-redis-sentinel-sentinel.juwan.svc.cluster.local', 26379) -], socket_timeout=5.0) - -# 获取主节点连接(用于写操作) -master = sentinel.master_for( - 'mymaster', - password=os.getenv('REDIS_PASSWORD'), - socket_timeout=3.0, - socket_connect_timeout=5.0, - socket_keepalive=True, - socket_keepalive_options={}, - connection_pool_kwargs={ - 'max_connections': 50 - } -) - -# 获取从节点连接(用于读操作) -slave = sentinel.slave_for( - 'mymaster', - password=os.getenv('REDIS_PASSWORD'), - socket_timeout=3.0 -) - -# 使用 -master.set('key', 'value') -value = slave.get('key') -``` - -#### Java (Spring Data Redis) - -```yaml -# application.yml -spring: - redis: - timeout: 3000ms - password: ${REDIS_PASSWORD} - sentinel: - master: mymaster - nodes: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - lettuce: - pool: - max-active: 10 - max-idle: 5 - min-idle: 2 - max-wait: 3000ms -``` - -```java -@Configuration -public class RedisConfig { - - @Bean - public RedisTemplate redisTemplate( - RedisConnectionFactory connectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(connectionFactory); - template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); - return template; - } -} -``` - ---- - -### 方案二:主从分离模式 ⭐ - -**优点** -- ✅ 读写性能优化 -- ✅ 配置清晰 -- ⚠️ 需要手动处理故障 - -**Go-Zero 配置** -```yaml -# app/users/rpc/etc/pb.yaml -Redis: - - Host: user-redis-master.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} - - Host: user-redis-replica.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} -``` - -**代码示例** -```go -type ServiceContext struct { - Config config.Config - RedisMaster *redis.Redis // 写操作 - RedisReplica *redis.Redis // 读操作 -} - -func NewServiceContext(c config.Config) *ServiceContext { - return &ServiceContext{ - Config: c, - RedisMaster: redis.MustNewRedis(c.Redis[0]), // master - RedisReplica: redis.MustNewRedis(c.Redis[1]), // replica - } -} - -// 写操作 -l.svcCtx.RedisMaster.Set("key", "value") - -// 读操作 -val, _ := l.svcCtx.RedisReplica.Get("key") -``` - ---- - -### 方案三:简单模式(仅测试环境) - -```yaml -Redis: - Host: user-redis-master.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} -``` - ---- - -### Kubernetes Deployment 配置 - -```yaml -# deploy/k8s/service/user/user-rpc.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: user-rpc - namespace: juwan -spec: - template: - spec: - containers: - - name: user-rpc - image: user-rpc:v1 - env: - # 数据库连接 - - name: DB_URI - valueFrom: - secretKeyRef: - name: user-db-app - key: uri - - # Redis 密码 - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: user-redis - key: password - - # 健康检查 - readinessProbe: - tcpSocket: - port: 9001 - initialDelaySeconds: 10 - periodSeconds: 10 - - livenessProbe: - tcpSocket: - port: 9001 - initialDelaySeconds: 15 - periodSeconds: 20 -``` - ---- - -## 📊 连接方式对比 - -| 连接方式 | 优点 | 缺点 | 故障转移 | 读写分离 | 复杂度 | 推荐度 | -|---------|------|------|---------|---------|--------|--------| -| **Sentinel 模式** | 自动高可用,客户端自动切换 | 配置稍复杂 | ✅ 自动 | ✅ 支持 | 中 | ⭐⭐⭐⭐⭐ | -| **主从分离** | 性能优化,逻辑清晰 | 需手动处理故障 | ❌ 手动 | ✅ 支持 | 中 | ⭐⭐⭐ | -| **仅连 Master** | 配置简单,强一致性 | 单点故障,读性能差 | ❌ 手动 | ❌ 不支持 | 低 | ⭐⭐ | -| **仅连 Replica** | 读性能好 | 只读,不能写入 | ❌ 手动 | ✅ 仅读 | 低 | ⭐ | -| **连 user-redis** | 极简单 | 性能差,不可靠 | ❌ 无 | ❌ 不支持 | 低 | ❌ | - ---- - -## 🎯 最佳实践建议 - -### 生产环境(强烈推荐) - -```yaml -# 使用 Sentinel 模式 -Redis: - Type: sentinel - MasterName: mymaster - SentinelAddrs: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Pass: ${REDIS_PASSWORD} - - # 连接池配置 - PoolSize: 10 - MinIdleConns: 5 - - # 超时配置 - DialTimeout: 5s - ReadTimeout: 3s - WriteTimeout: 3s - - # 重试配置 - MaxRetries: 3 -``` - -**理由** -- ✅ 自动故障转移,RTO < 30秒 -- ✅ 客户端无感知切换 -- ✅ 无需人工介入 -- ✅ 久经考验的成熟方案 - ---- - -### 开发/测试环境 - -```yaml -# 简化配置,直连主节点 -Redis: - Host: user-redis-master.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} -``` - -或使用 port-forward: -```bash -kubectl port-forward -n juwan svc/user-redis-master 6379:6379 -``` - ---- - -### 性能优化建议 - -#### 1. 连接池配置 -```go -PoolSize: runtime.NumCPU() * 2, // CPU 数量的 2 倍 -MinIdleConns: runtime.NumCPU(), // CPU 数量 -MaxConnAge: 30 * time.Minute, // 连接最大存活时间 -``` - -#### 2. 超时配置 -```go -DialTimeout: 5 * time.Second, // 连接超时 -ReadTimeout: 3 * time.Second, // 读超时 -WriteTimeout: 3 * time.Second, // 写超时 -PoolTimeout: 4 * time.Second, // 获取连接超时 -``` - -#### 3. 命令优化 -```go -// ❌ 避免:循环中多次调用 -for i := 0; i < 1000; i++ { - rdb.Set(ctx, fmt.Sprintf("key:%d", i), i) -} - -// ✅ 推荐:使用 Pipeline -pipe := rdb.Pipeline() -for i := 0; i < 1000; i++ { - pipe.Set(ctx, fmt.Sprintf("key:%d", i), i, 0) -} -pipe.Exec(ctx) -``` - -#### 4. 缓存策略 -```go -// Cache-Aside Pattern -func GetUser(id int64) (*User, error) { - // 1. 先查缓存 - cacheKey := fmt.Sprintf("user:%d", id) - cached, err := rdb.Get(ctx, cacheKey).Result() - if err == nil { - var user User - json.Unmarshal([]byte(cached), &user) - return &user, nil - } - - // 2. 缓存未命中,查数据库 - user := queryFromDB(id) - - // 3. 写入缓存 - userJSON, _ := json.Marshal(user) - rdb.Set(ctx, cacheKey, userJSON, 1*time.Hour) - - return user, nil -} -``` - ---- - -### 监控配置 - -#### Prometheus 指标采集 -```yaml -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: redis-exporter - namespace: juwan -spec: - selector: - matchLabels: - app: user-redis - endpoints: - - port: redis-exporter # 端口 9121 - interval: 30s - path: /metrics -``` - -#### 关键指标监控 -```yaml -# 告警规则示例 -groups: - - name: redis - rules: - # Redis 实例宕机 - - alert: RedisDown - expr: redis_up == 0 - for: 1m - annotations: - summary: "Redis instance down" - - # 内存使用率过高 - - alert: RedisMemoryHigh - expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.9 - for: 5m - annotations: - summary: "Redis memory usage > 90%" - - # 连接数过高 - - alert: RedisConnectionsHigh - expr: redis_connected_clients > 1000 - for: 5m - annotations: - summary: "Redis connections > 1000" -``` - ---- - -### 安全建议 - -#### 1. 密码管理 -```bash -# 定期轮换密码 -kubectl create secret generic user-redis \ - --from-literal=password=$(openssl rand -base64 32) \ - --dry-run=client -o yaml | kubectl apply -f - - -# 重启 Redis Pods 使新密码生效 -kubectl rollout restart statefulset/user-redis -n juwan -``` - -#### 2. 网络策略 -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: redis-access - namespace: juwan -spec: - podSelector: - matchLabels: - app: user-redis - policyTypes: - - Ingress - ingress: - # 只允许同命名空间的 user-rpc 访问 - - from: - - podSelector: - matchLabels: - app: user-rpc - ports: - - protocol: TCP - port: 6379 -``` - -#### 3. TLS 加密(可选) -```yaml -# Redis TLS 配置 -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisReplication -metadata: - name: user-redis -spec: - TLS: - enabled: true - secret: - secretName: redis-tls-cert -``` - ---- - -## 🔍 故障排查 - -### 1. 连接失败 - -**症状** -``` -Error: dial tcp 10.97.120.76:6379: i/o timeout -``` - -**排查步骤** -```bash -# 1. 检查 Service 是否存在 -kubectl get svc user-redis-master -n juwan - -# 2. 检查 Endpoints -kubectl get endpoints user-redis-master -n juwan - -# 3. 检查 Pod 状态 -kubectl get pods -l app=user-redis -n juwan - -# 4. 测试网络连通性 -kubectl run -it --rm netshoot --image=nicolaka/netshoot --restart=Never -n juwan -- \ - nc -zv user-redis-master 6379 - -# 5. 查看 Pod 日志 -kubectl logs user-redis-0 -n juwan -c redis -``` - ---- - -### 2. 认证失败 - -**症状** -``` -Error: NOAUTH Authentication required -``` - -**解决方法** -```bash -# 1. 确认 Secret 存在 -kubectl get secret user-redis -n juwan - -# 2. 验证密码 -PASSWORD=$(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) -echo $PASSWORD - -# 3. 测试连接 -kubectl exec -it user-redis-0 -n juwan -- redis-cli -a $PASSWORD PING -``` - ---- - -### 3. 主从复制异常 - -**症状** -``` -Warning: Redis replica lag is high -``` - -**排查** -```bash -# 在主节点执行 -kubectl exec -it user-redis-0 -n juwan -- redis-cli -a INFO replication - -# 查看输出 -# role:master -# connected_slaves:2 -# slave0:ip=10.244.1.10,port=6379,state=online,offset=1234,lag=0 -# slave1:ip=10.244.2.15,port=6379,state=online,offset=1234,lag=0 -``` - -**如果 lag 过大** -```bash -# 检查网络延迟 -kubectl exec -it user-redis-0 -n juwan -- ping user-redis-1 - -# 检查 Redis 性能 -kubectl exec -it user-redis-0 -n juwan -- redis-cli -a INFO stats -``` - ---- - -### 4. Sentinel 无法发现主节点 - -**症状** -``` -Error: sentinel: no master found -``` - -**排查** -```bash -# 1. 检查 Sentinel 状态 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL masters - -# 2. 检查主节点地址 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster - -# 3. 查看 Sentinel 日志 -kubectl logs user-redis-sentinel-sentinel-0 -n juwan - -# 4. 手动触发故障转移(慎用) -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL failover mymaster -``` - ---- - -### 5. 性能问题 - -**慢查询分析** -```bash -# 查看慢查询 -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a SLOWLOG GET 10 - -# 设置慢查询阈值(10ms) -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a CONFIG SET slowlog-log-slower-than 10000 -``` - -**命令统计** -```bash -# 查看命令统计 -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a INFO commandstats -``` - -**内存分析** -```bash -# 查看内存使用 -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a INFO memory - -# 查看大 key -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a --bigkeys -``` - ---- - -## 📚 参考资源 - -### 官方文档 -- [Redis Sentinel Documentation](https://redis.io/docs/management/sentinel/) -- [go-redis Documentation](https://redis.uptrace.dev/) -- [OpsTree Redis Operator](https://ot-redis-operator.netlify.app/) - -### 客户端库 -- Go: [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) -- Python: [redis-py](https://github.com/redis/redis-py) -- Java: [Spring Data Redis](https://spring.io/projects/spring-data-redis) -- Node.js: [ioredis](https://github.com/luin/ioredis) - -### 监控工具 -- [RedisInsight](https://redis.com/redis-enterprise/redis-insight/) -- [Redis Exporter](https://github.com/oliver006/redis_exporter) -- Grafana Dashboard: [Redis Dashboard 11835](https://grafana.com/grafana/dashboards/11835) - ---- - -## 📝 更新日志 - -| 日期 | 版本 | 变更内容 | -|------|------|---------| -| 2026-02-22 | 1.0 | 初始版本,包含完整的 Service 介绍和连接指南 | - ---- - -**文档维护者**: DevOps Team -**最后更新**: 2026年2月22日 -**下一次审查**: 2026年3月22日 +# Redis Services 连接指南 + +**环境:** juwan namespace +**Redis 版本:** 7.0.12 +**部署模式:** RedisReplication + RedisSentinel +**文档日期:** 2026年2月22日 + +--- + +## 📋 目录 + +1. [Service 列表总览](#service-列表总览) +2. [Redis 数据层 Service 详解](#redis-数据层-service-详解) +3. [Sentinel 监控层 Service 详解](#sentinel-监控层-service-详解) +4. [管理工具连接方式](#管理工具连接方式) +5. [应用服务连接方式](#应用服务连接方式) +6. [连接方式对比](#连接方式对比) +7. [最佳实践建议](#最佳实践建议) +8. [故障排查](#故障排查) + +--- + +## 📊 Service 列表总览 + +当前集群中部署的 Redis 相关 Service: + +| Service 名称 | 类型 | Cluster IP | 端口 | 用途 | +|-------------|------|-----------|------|------| +| **user-redis** | ClusterIP | 10.103.91.84 | 6379, 9121 | 通用访问 + 监控 | +| **user-redis-additional** | ClusterIP | 10.107.228.48 | 6379 | 额外访问入口 | +| **user-redis-headless** | ClusterIP(None) | None | 6379 | Pod 间直接访问 | +| **user-redis-master** ⭐ | ClusterIP | 10.97.120.76 | 6379 | 主节点访问 | +| **user-redis-replica** ⭐ | ClusterIP | 10.100.213.103 | 6379 | 从节点访问 | +| **user-redis-sentinel-sentinel** ⭐⭐⭐ | ClusterIP | 10.105.28.231 | 26379 | Sentinel 访问 | +| **user-redis-sentinel-sentinel-additional** | ClusterIP | 10.97.111.42 | 26379 | 额外 Sentinel 入口 | +| **user-redis-sentinel-sentinel-headless** | ClusterIP(None) | None | 26379 | Sentinel 间通信 | + +**图例:** +- ⭐⭐⭐ 生产环境强烈推荐 +- ⭐ 生产环境可用 +- 无标记:特殊场景或内部使用 + +--- + +## 🔴 Redis 数据层 Service 详解 + +### 1. user-redis-master ⭐ + +**基本信息** +```yaml +名称: user-redis-master +类型: ClusterIP +IP: 10.97.120.76 +端口: 6379/TCP +DNS: user-redis-master.juwan.svc.cluster.local +``` + +**功能特点** +- 🎯 自动追踪当前 Redis 主节点 +- ✅ 确保所有写操作到达主节点 +- 🔄 故障转移后自动指向新主节点 +- 💪 提供最强一致性保证 + +**适用场景** +- ✅ 所有写操作(SET, HSET, ZADD 等) +- ✅ 需要强一致性的读操作 +- ✅ 事务操作(MULTI/EXEC) +- ❌ 不适合高并发读请求 + +**连接示例** +```go +// Go - go-redis +rdb := redis.NewClient(&redis.Options{ + Addr: "user-redis-master.juwan.svc.cluster.local:6379", + Password: os.Getenv("REDIS_PASSWORD"), + DB: 0, +}) + +// 写操作 +err := rdb.Set(ctx, "user:1001", "John Doe", 0).Err() +``` + +```bash +# CLI 测试 +kubectl run -it --rm redis-cli --image=redis:7.0.12 --restart=Never -n juwan -- \ + redis-cli -h user-redis-master -a SET test "hello" +``` + +--- + +### 2. user-redis-replica ⭐ + +**基本信息** +```yaml +名称: user-redis-replica +类型: ClusterIP +IP: 10.100.213.103 +端口: 6379/TCP +DNS: user-redis-replica.juwan.svc.cluster.local +``` + +**功能特点** +- 📖 负载均衡到所有从节点(当前 2 个) +- ⚡ 分散读请求,提升吞吐量 +- 🕐 可能存在轻微的复制延迟(通常 < 100ms) +- 🚫 只读模式,写操作会失败 + +**适用场景** +- ✅ 高并发读请求 +- ✅ 查询操作(GET, HGET, ZRANGE 等) +- ✅ 统计分析类查询 +- ⚠️ 对数据实时性要求不高的场景 +- ❌ 不能用于写操作 + +**连接示例** +```go +// Go - 读写分离配置 +masterClient := redis.NewClient(&redis.Options{ + Addr: "user-redis-master.juwan.svc.cluster.local:6379", + Password: os.Getenv("REDIS_PASSWORD"), +}) + +replicaClient := redis.NewClient(&redis.Options{ + Addr: "user-redis-replica.juwan.svc.cluster.local:6379", + Password: os.Getenv("REDIS_PASSWORD"), + ReadOnly: true, +}) + +// 写操作用 master +masterClient.Set(ctx, "counter", 100, 0) + +// 读操作用 replica +val, err := replicaClient.Get(ctx, "counter").Result() +``` + +--- + +### 3. user-redis + +**基本信息** +```yaml +名称: user-redis +类型: ClusterIP +IP: 10.103.91.84 +端口: 6379/TCP (Redis), 9121/TCP (Exporter) +DNS: user-redis.juwan.svc.cluster.local +``` + +**功能特点** +- 🔀 负载均衡到所有 Redis 节点(主 + 从) +- 📊 端口 9121 暴露 Prometheus 指标 +- ⚠️ 写操作可能路由到从节点导致失败 + +**适用场景** +- ✅ Prometheus 监控抓取(端口 9121) +- ⚠️ 测试环境的简单访问 +- ❌ 不推荐生产环境读写操作 + +**监控配置** +```yaml +# Prometheus ServiceMonitor +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: redis-metrics + namespace: juwan +spec: + selector: + matchLabels: + app: redis + endpoints: + - port: redis-exporter + interval: 30s + path: /metrics +``` + +--- + +### 4. user-redis-additional + +**基本信息** +```yaml +名称: user-redis-additional +类型: ClusterIP +IP: 10.107.228.48 +端口: 6379/TCP +``` + +**功能特点** +- 功能类似 user-redis +- 提供额外的访问入口 +- 用于多租户或网络隔离场景 + +**适用场景** +- 特殊网络策略场景 +- 多应用隔离访问 +- 备用访问点 + +--- + +### 5. user-redis-headless + +**基本信息** +```yaml +名称: user-redis-headless +类型: ClusterIP (Headless - None) +端口: 6379/TCP +DNS: + - user-redis-0.user-redis-headless.juwan.svc.cluster.local + - user-redis-1.user-redis-headless.juwan.svc.cluster.local + - user-redis-2.user-redis-headless.juwan.svc.cluster.local +``` + +**功能特点** +- 🎯 直接返回所有 Pod IP,不做负载均衡 +- 🔗 用于 StatefulSet Pod 间通信 +- 📡 Redis 主从复制使用此服务发现 + +**适用场景** +- ✅ 内部复制通信 +- ✅ 集群管理操作 +- ✅ 需要直接访问特定 Pod +- ❌ 不适合应用层使用 + +**直接访问示例** +```bash +# 直接连接 user-redis-0 +redis-cli -h user-redis-0.user-redis-headless.juwan.svc.cluster.local -a + +# DNS 解析会返回具体 Pod IP +nslookup user-redis-headless.juwan.svc.cluster.local +``` + +--- + +## 🟡 Sentinel 监控层 Service 详解 + +### 1. user-redis-sentinel-sentinel ⭐⭐⭐ + +**基本信息** +```yaml +名称: user-redis-sentinel-sentinel +类型: ClusterIP +IP: 10.105.28.231 +端口: 26379/TCP +DNS: user-redis-sentinel-sentinel.juwan.svc.cluster.local +``` + +**功能特点** +- 🛡️ 提供高可用 Redis 访问 +- 🔄 自动发现主节点 +- ⚡ 主节点故障时自动切换 +- 📍 客户端自动跟踪主节点变化 + +**Sentinel 架构** +``` +应用 → Sentinel Service → Sentinel 节点 (3个) + ↓ + 监控 Redis 集群 + ↓ + 自动发现当前主节点位置 +``` + +**适用场景** +- ✅✅✅ **生产环境强烈推荐** +- ✅ 需要自动故障转移 +- ✅ 高可用架构 +- ✅ 无需手动处理主从切换 + +**连接示例** +```go +// Go - go-redis Sentinel 模式 +rdb := redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: "mymaster", + SentinelAddrs: []string{ + "user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379", + }, + Password: os.Getenv("REDIS_PASSWORD"), + DB: 0, + + // 连接池配置 + PoolSize: 10, + MinIdleConns: 5, + + // 超时配置 + DialTimeout: 5 * time.Second, + ReadTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, +}) + +// 使用方式与普通客户端完全一致 +err := rdb.Set(ctx, "key", "value", 0).Err() +val, err := rdb.Get(ctx, "key").Result() +``` + +```python +# Python - redis-py Sentinel 模式 +from redis.sentinel import Sentinel + +sentinel = Sentinel([ + ('user-redis-sentinel-sentinel.juwan.svc.cluster.local', 26379) +], socket_timeout=0.5) + +# 获取主节点(写) +master = sentinel.master_for('mymaster', + password=os.getenv('REDIS_PASSWORD'), + socket_timeout=0.5) +master.set('key', 'value') + +# 获取从节点(读) +slave = sentinel.slave_for('mymaster', + password=os.getenv('REDIS_PASSWORD'), + socket_timeout=0.5) +value = slave.get('key') +``` + +```yaml +# Spring Boot application.yml +spring: + redis: + sentinel: + master: mymaster + nodes: + - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + password: ${REDIS_PASSWORD} +``` + +**Sentinel 命令查询** +```bash +# 查看主节点信息 +kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ + redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster + +# 查看所有监控的主节点 +kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ + redis-cli -p 26379 SENTINEL masters + +# 查看从节点列表 +kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ + redis-cli -p 26379 SENTINEL slaves mymaster + +# 查看 Sentinel 节点 +kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ + redis-cli -p 26379 SENTINEL sentinels mymaster +``` + +--- + +### 2. user-redis-sentinel-sentinel-additional + +**基本信息** +```yaml +名称: user-redis-sentinel-sentinel-additional +类型: ClusterIP +IP: 10.97.111.42 +端口: 26379/TCP +``` + +**功能特点** +- 功能同 user-redis-sentinel-sentinel +- 提供额外访问入口 +- 用于多客户端分离 + +**适用场景** +- 多应用共享 Redis 时的访问隔离 +- 网络策略要求 +- 流量分离 + +--- + +### 3. user-redis-sentinel-sentinel-headless + +**基本信息** +```yaml +名称: user-redis-sentinel-sentinel-headless +类型: ClusterIP (Headless - None) +端口: 26379/TCP +``` + +**功能特点** +- Sentinel 节点间通信 +- 选举和投票 +- 状态同步 + +**适用场景** +- 内部使用,应用层无需关注 + +--- + +## 🔧 管理工具连接方式 + +### 使用 kubectl port-forward(推荐) + +#### 方式一:连接主节点 +```bash +# 转发主节点服务到本地 +kubectl port-forward -n juwan svc/user-redis-master 6379:6379 + +# 或直接转发到 Pod +kubectl port-forward -n juwan pod/user-redis-0 6379:6379 +``` + +然后在管理工具中配置: +- **Host**: localhost +- **Port**: 6379 +- **Password**: (见下方获取方法) + +#### 方式二:使用 LoadBalancer(生产不推荐) +```yaml +# 临时暴露服务(仅用于调试) +apiVersion: v1 +kind: Service +metadata: + name: redis-external + namespace: juwan +spec: + type: LoadBalancer + selector: + app: user-redis + ports: + - port: 6379 + targetPort: 6379 +``` + +--- + +### 获取 Redis 密码 + +```bash +# 方式一:直接输出 +kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d + +# 方式二:设置为环境变量 +export REDIS_PASSWORD=$(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) +echo $REDIS_PASSWORD + +# 方式三:保存到文件 +kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d > redis-password.txt +``` + +--- + +### 常用管理工具配置 + +#### Redis Desktop Manager (RedisInsight) +``` +Connection Name: juwan-user-redis +Host: localhost (使用 port-forward) +Port: 6379 +Username: (留空) +Password: (从 Secret 获取) +``` + +#### redis-cli +```bash +# 在集群内访问 +kubectl exec -it user-redis-0 -n juwan -- redis-cli -a + +# 从本地访问(需要 port-forward) +redis-cli -h localhost -p 6379 -a + +# 常用命令 +INFO replication # 查看复制状态 +INFO stats # 查看统计信息 +CLUSTER INFO # 查看集群信息 +KEYS * # 查看所有 key(生产谨慎使用) +``` + +--- + +## 💻 应用服务连接方式 + +### 方案一:Sentinel 模式(生产强烈推荐)⭐⭐⭐ + +**优点** +- ✅ 自动故障转移 +- ✅ 高可用 +- ✅ 无需手动切换 +- ✅ 客户端自动重连 +- ✅ 支持读写分离 + +#### Go-Zero 配置 + +**配置文件** `app/users/rpc/etc/pb.yaml` +```yaml +Name: pb.rpc +ListenOn: 0.0.0.0:9001 + +# Redis Sentinel 配置 +Redis: + Type: sentinel + MasterName: mymaster + SentinelAddrs: + - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Pass: ${REDIS_PASSWORD} # 从环境变量读取 + +Etcd: + Hosts: + - etcd-service.juwan.svc.cluster.local:2379 + Key: pb.rpc +``` + +**Config 结构** `app/users/rpc/internal/config/config.go` +```go +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/zrpc" +) + +type Config struct { + zrpc.RpcServerConf + Redis redis.RedisConf +} +``` + +**初始化 Redis** `app/users/rpc/internal/svc/serviceContext.go` +```go +package svc + +import ( + "github.com/zeromicro/go-zero/core/stores/redis" + "juwan-backend/app/users/rpc/internal/config" +) + +type ServiceContext struct { + Config config.Config + Redis *redis.Redis +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + Redis: redis.MustNewRedis(c.Redis), + } +} +``` + +**使用示例** `app/users/rpc/internal/logic/getUsersByIdLogic.go` +```go +func (l *GetUsersByIdLogic) GetUsersById(in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { + // 尝试从缓存获取 + cacheKey := fmt.Sprintf("user:%d", in.Id) + cached, err := l.svcCtx.Redis.Get(cacheKey) + if err == nil && cached != "" { + // 缓存命中 + var user pb.User + json.Unmarshal([]byte(cached), &user) + return &pb.GetUsersByIdResp{User: &user}, nil + } + + // 从数据库查询 + user := l.fetchUserFromDB(in.Id) + + // 写入缓存 + userJSON, _ := json.Marshal(user) + l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) // 1小时过期 + + return &pb.GetUsersByIdResp{User: user}, nil +} +``` + +#### Go (原生 go-redis) + +```go +package main + +import ( + "context" + "github.com/redis/go-redis/v9" + "time" +) + +func NewRedisClient() *redis.Client { + rdb := redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: "mymaster", + SentinelAddrs: []string{ + "user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379", + }, + Password: os.Getenv("REDIS_PASSWORD"), + DB: 0, + + // 连接池配置 + PoolSize: 10, + MinIdleConns: 5, + + // 超时配置 + DialTimeout: 5 * time.Second, + ReadTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, + + // 重试配置 + MaxRetries: 3, + MinRetryBackoff: 8 * time.Millisecond, + MaxRetryBackoff: 512 * time.Millisecond, + }) + + // 测试连接 + ctx := context.Background() + if err := rdb.Ping(ctx).Err(); err != nil { + panic(err) + } + + return rdb +} +``` + +#### Python (redis-py) + +```python +from redis.sentinel import Sentinel +import os + +# 初始化 Sentinel +sentinel = Sentinel([ + ('user-redis-sentinel-sentinel.juwan.svc.cluster.local', 26379) +], socket_timeout=5.0) + +# 获取主节点连接(用于写操作) +master = sentinel.master_for( + 'mymaster', + password=os.getenv('REDIS_PASSWORD'), + socket_timeout=3.0, + socket_connect_timeout=5.0, + socket_keepalive=True, + socket_keepalive_options={}, + connection_pool_kwargs={ + 'max_connections': 50 + } +) + +# 获取从节点连接(用于读操作) +slave = sentinel.slave_for( + 'mymaster', + password=os.getenv('REDIS_PASSWORD'), + socket_timeout=3.0 +) + +# 使用 +master.set('key', 'value') +value = slave.get('key') +``` + +#### Java (Spring Data Redis) + +```yaml +# application.yml +spring: + redis: + timeout: 3000ms + password: ${REDIS_PASSWORD} + sentinel: + master: mymaster + nodes: + - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + lettuce: + pool: + max-active: 10 + max-idle: 5 + min-idle: 2 + max-wait: 3000ms +``` + +```java +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate( + RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + return template; + } +} +``` + +--- + +### 方案二:主从分离模式 ⭐ + +**优点** +- ✅ 读写性能优化 +- ✅ 配置清晰 +- ⚠️ 需要手动处理故障 + +**Go-Zero 配置** +```yaml +# app/users/rpc/etc/pb.yaml +Redis: + - Host: user-redis-master.juwan.svc.cluster.local:6379 + Type: node + Pass: ${REDIS_PASSWORD} + - Host: user-redis-replica.juwan.svc.cluster.local:6379 + Type: node + Pass: ${REDIS_PASSWORD} +``` + +**代码示例** +```go +type ServiceContext struct { + Config config.Config + RedisMaster *redis.Redis // 写操作 + RedisReplica *redis.Redis // 读操作 +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + RedisMaster: redis.MustNewRedis(c.Redis[0]), // master + RedisReplica: redis.MustNewRedis(c.Redis[1]), // replica + } +} + +// 写操作 +l.svcCtx.RedisMaster.Set("key", "value") + +// 读操作 +val, _ := l.svcCtx.RedisReplica.Get("key") +``` + +--- + +### 方案三:简单模式(仅测试环境) + +```yaml +Redis: + Host: user-redis-master.juwan.svc.cluster.local:6379 + Type: node + Pass: ${REDIS_PASSWORD} +``` + +--- + +### Kubernetes Deployment 配置 + +```yaml +# deploy/k8s/service/user/user-rpc.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-rpc + namespace: juwan +spec: + template: + spec: + containers: + - name: user-rpc + image: user-rpc:v1 + env: + # 数据库连接 + - name: DB_URI + valueFrom: + secretKeyRef: + name: user-db-app + key: uri + + # Redis 密码 + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: user-redis + key: password + + # 健康检查 + readinessProbe: + tcpSocket: + port: 9001 + initialDelaySeconds: 10 + periodSeconds: 10 + + livenessProbe: + tcpSocket: + port: 9001 + initialDelaySeconds: 15 + periodSeconds: 20 +``` + +--- + +## 📊 连接方式对比 + +| 连接方式 | 优点 | 缺点 | 故障转移 | 读写分离 | 复杂度 | 推荐度 | +|---------|------|------|---------|---------|--------|--------| +| **Sentinel 模式** | 自动高可用,客户端自动切换 | 配置稍复杂 | ✅ 自动 | ✅ 支持 | 中 | ⭐⭐⭐⭐⭐ | +| **主从分离** | 性能优化,逻辑清晰 | 需手动处理故障 | ❌ 手动 | ✅ 支持 | 中 | ⭐⭐⭐ | +| **仅连 Master** | 配置简单,强一致性 | 单点故障,读性能差 | ❌ 手动 | ❌ 不支持 | 低 | ⭐⭐ | +| **仅连 Replica** | 读性能好 | 只读,不能写入 | ❌ 手动 | ✅ 仅读 | 低 | ⭐ | +| **连 user-redis** | 极简单 | 性能差,不可靠 | ❌ 无 | ❌ 不支持 | 低 | ❌ | + +--- + +## 🎯 最佳实践建议 + +### 生产环境(强烈推荐) + +```yaml +# 使用 Sentinel 模式 +Redis: + Type: sentinel + MasterName: mymaster + SentinelAddrs: + - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Pass: ${REDIS_PASSWORD} + + # 连接池配置 + PoolSize: 10 + MinIdleConns: 5 + + # 超时配置 + DialTimeout: 5s + ReadTimeout: 3s + WriteTimeout: 3s + + # 重试配置 + MaxRetries: 3 +``` + +**理由** +- ✅ 自动故障转移,RTO < 30秒 +- ✅ 客户端无感知切换 +- ✅ 无需人工介入 +- ✅ 久经考验的成熟方案 + +--- + +### 开发/测试环境 + +```yaml +# 简化配置,直连主节点 +Redis: + Host: user-redis-master.juwan.svc.cluster.local:6379 + Type: node + Pass: ${REDIS_PASSWORD} +``` + +或使用 port-forward: +```bash +kubectl port-forward -n juwan svc/user-redis-master 6379:6379 +``` + +--- + +### 性能优化建议 + +#### 1. 连接池配置 +```go +PoolSize: runtime.NumCPU() * 2, // CPU 数量的 2 倍 +MinIdleConns: runtime.NumCPU(), // CPU 数量 +MaxConnAge: 30 * time.Minute, // 连接最大存活时间 +``` + +#### 2. 超时配置 +```go +DialTimeout: 5 * time.Second, // 连接超时 +ReadTimeout: 3 * time.Second, // 读超时 +WriteTimeout: 3 * time.Second, // 写超时 +PoolTimeout: 4 * time.Second, // 获取连接超时 +``` + +#### 3. 命令优化 +```go +// ❌ 避免:循环中多次调用 +for i := 0; i < 1000; i++ { + rdb.Set(ctx, fmt.Sprintf("key:%d", i), i) +} + +// ✅ 推荐:使用 Pipeline +pipe := rdb.Pipeline() +for i := 0; i < 1000; i++ { + pipe.Set(ctx, fmt.Sprintf("key:%d", i), i, 0) +} +pipe.Exec(ctx) +``` + +#### 4. 缓存策略 +```go +// Cache-Aside Pattern +func GetUser(id int64) (*User, error) { + // 1. 先查缓存 + cacheKey := fmt.Sprintf("user:%d", id) + cached, err := rdb.Get(ctx, cacheKey).Result() + if err == nil { + var user User + json.Unmarshal([]byte(cached), &user) + return &user, nil + } + + // 2. 缓存未命中,查数据库 + user := queryFromDB(id) + + // 3. 写入缓存 + userJSON, _ := json.Marshal(user) + rdb.Set(ctx, cacheKey, userJSON, 1*time.Hour) + + return user, nil +} +``` + +--- + +### 监控配置 + +#### Prometheus 指标采集 +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: redis-exporter + namespace: juwan +spec: + selector: + matchLabels: + app: user-redis + endpoints: + - port: redis-exporter # 端口 9121 + interval: 30s + path: /metrics +``` + +#### 关键指标监控 +```yaml +# 告警规则示例 +groups: + - name: redis + rules: + # Redis 实例宕机 + - alert: RedisDown + expr: redis_up == 0 + for: 1m + annotations: + summary: "Redis instance down" + + # 内存使用率过高 + - alert: RedisMemoryHigh + expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.9 + for: 5m + annotations: + summary: "Redis memory usage > 90%" + + # 连接数过高 + - alert: RedisConnectionsHigh + expr: redis_connected_clients > 1000 + for: 5m + annotations: + summary: "Redis connections > 1000" +``` + +--- + +### 安全建议 + +#### 1. 密码管理 +```bash +# 定期轮换密码 +kubectl create secret generic user-redis \ + --from-literal=password=$(openssl rand -base64 32) \ + --dry-run=client -o yaml | kubectl apply -f - + +# 重启 Redis Pods 使新密码生效 +kubectl rollout restart statefulset/user-redis -n juwan +``` + +#### 2. 网络策略 +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: redis-access + namespace: juwan +spec: + podSelector: + matchLabels: + app: user-redis + policyTypes: + - Ingress + ingress: + # 只允许同命名空间的 user-rpc 访问 + - from: + - podSelector: + matchLabels: + app: user-rpc + ports: + - protocol: TCP + port: 6379 +``` + +#### 3. TLS 加密(可选) +```yaml +# Redis TLS 配置 +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisReplication +metadata: + name: user-redis +spec: + TLS: + enabled: true + secret: + secretName: redis-tls-cert +``` + +--- + +## 🔍 故障排查 + +### 1. 连接失败 + +**症状** +``` +Error: dial tcp 10.97.120.76:6379: i/o timeout +``` + +**排查步骤** +```bash +# 1. 检查 Service 是否存在 +kubectl get svc user-redis-master -n juwan + +# 2. 检查 Endpoints +kubectl get endpoints user-redis-master -n juwan + +# 3. 检查 Pod 状态 +kubectl get pods -l app=user-redis -n juwan + +# 4. 测试网络连通性 +kubectl run -it --rm netshoot --image=nicolaka/netshoot --restart=Never -n juwan -- \ + nc -zv user-redis-master 6379 + +# 5. 查看 Pod 日志 +kubectl logs user-redis-0 -n juwan -c redis +``` + +--- + +### 2. 认证失败 + +**症状** +``` +Error: NOAUTH Authentication required +``` + +**解决方法** +```bash +# 1. 确认 Secret 存在 +kubectl get secret user-redis -n juwan + +# 2. 验证密码 +PASSWORD=$(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) +echo $PASSWORD + +# 3. 测试连接 +kubectl exec -it user-redis-0 -n juwan -- redis-cli -a $PASSWORD PING +``` + +--- + +### 3. 主从复制异常 + +**症状** +``` +Warning: Redis replica lag is high +``` + +**排查** +```bash +# 在主节点执行 +kubectl exec -it user-redis-0 -n juwan -- redis-cli -a INFO replication + +# 查看输出 +# role:master +# connected_slaves:2 +# slave0:ip=10.244.1.10,port=6379,state=online,offset=1234,lag=0 +# slave1:ip=10.244.2.15,port=6379,state=online,offset=1234,lag=0 +``` + +**如果 lag 过大** +```bash +# 检查网络延迟 +kubectl exec -it user-redis-0 -n juwan -- ping user-redis-1 + +# 检查 Redis 性能 +kubectl exec -it user-redis-0 -n juwan -- redis-cli -a INFO stats +``` + +--- + +### 4. Sentinel 无法发现主节点 + +**症状** +``` +Error: sentinel: no master found +``` + +**排查** +```bash +# 1. 检查 Sentinel 状态 +kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ + redis-cli -p 26379 SENTINEL masters + +# 2. 检查主节点地址 +kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ + redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster + +# 3. 查看 Sentinel 日志 +kubectl logs user-redis-sentinel-sentinel-0 -n juwan + +# 4. 手动触发故障转移(慎用) +kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ + redis-cli -p 26379 SENTINEL failover mymaster +``` + +--- + +### 5. 性能问题 + +**慢查询分析** +```bash +# 查看慢查询 +kubectl exec -it user-redis-0 -n juwan -- \ + redis-cli -a SLOWLOG GET 10 + +# 设置慢查询阈值(10ms) +kubectl exec -it user-redis-0 -n juwan -- \ + redis-cli -a CONFIG SET slowlog-log-slower-than 10000 +``` + +**命令统计** +```bash +# 查看命令统计 +kubectl exec -it user-redis-0 -n juwan -- \ + redis-cli -a INFO commandstats +``` + +**内存分析** +```bash +# 查看内存使用 +kubectl exec -it user-redis-0 -n juwan -- \ + redis-cli -a INFO memory + +# 查看大 key +kubectl exec -it user-redis-0 -n juwan -- \ + redis-cli -a --bigkeys +``` + +--- + +## 📚 参考资源 + +### 官方文档 +- [Redis Sentinel Documentation](https://redis.io/docs/management/sentinel/) +- [go-redis Documentation](https://redis.uptrace.dev/) +- [OpsTree Redis Operator](https://ot-redis-operator.netlify.app/) + +### 客户端库 +- Go: [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) +- Python: [redis-py](https://github.com/redis/redis-py) +- Java: [Spring Data Redis](https://spring.io/projects/spring-data-redis) +- Node.js: [ioredis](https://github.com/luin/ioredis) + +### 监控工具 +- [RedisInsight](https://redis.com/redis-enterprise/redis-insight/) +- [Redis Exporter](https://github.com/oliver006/redis_exporter) +- Grafana Dashboard: [Redis Dashboard 11835](https://grafana.com/grafana/dashboards/11835) + +--- + +## 📝 更新日志 + +| 日期 | 版本 | 变更内容 | +|------|------|---------| +| 2026-02-22 | 1.0 | 初始版本,包含完整的 Service 介绍和连接指南 | + +--- + +**文档维护者**: DevOps Team +**最后更新**: 2026年2月22日 +**下一次审查**: 2026年3月22日 diff --git a/docs/redis-username-discovery.md b/docs/redis-username-discovery.md index 902a502..312026a 100644 --- a/docs/redis-username-discovery.md +++ b/docs/redis-username-discovery.md @@ -1,1068 +1,1068 @@ -# Redis 用户名查找和认证配置指南 - -**问题日期:** 2026年2月22日 -**环境:** juwan namespace -**Redis 版本:** 7.0.12 -**部署模式:** RedisReplication (OpsTree Operator) - ---- - -## 📋 目录 - -1. [问题背景](#问题背景) -2. [Redis 认证机制演进](#redis-认证机制演进) -3. [诊断过程](#诊断过程) -4. [用户名发现结果](#用户名发现结果) -5. [各语言连接配置](#各语言连接配置) -6. [常见问题解答](#常见问题解答) -7. [安全建议](#安全建议) - ---- - -## 🎯 问题背景 - -### 问题描述 -在配置应用连接 Redis 时,发现只有密码信息(存储在 Kubernetes Secret `user-redis` 中),但不清楚: -- Redis 是否需要用户名? -- 如果需要,用户名是什么? -- 如何查找 Redis 的用户名配置? - -### 初始已知信息 -```yaml -Secret: user-redis -Namespace: juwan -Key: password -Value: (base64 编码的密码) -``` - -### 疑问 -- 使用 RedisInsight 等管理工具时,Username 字段应该填什么? -- 应用代码中是否需要配置 Username? -- 不同 Redis 版本的认证方式是否有差异? - ---- - -## 📚 Redis 认证机制演进 - -### Redis 6.0 之前:传统密码认证 - -**特点:** -- ❌ 不支持多用户 -- ✅ 只需配置一个全局密码 -- 🔧 配置项:`requirepass` -- 👤 隐式用户名:`default`(客户端不需要指定) - -**配置示例:** -```conf -# redis.conf -requirepass mypassword -``` - -**连接方式:** -```bash -# 只需要密码 -redis-cli -h host -p 6379 -a mypassword - -# 或使用 AUTH 命令 -redis-cli -h host -p 6379 -> AUTH mypassword -``` - -**优点:** -- ✅ 配置简单 -- ✅ 向后兼容 - -**缺点:** -- ❌ 所有客户端共享同一密码 -- ❌ 无法区分不同应用的访问权限 -- ❌ 无法限制特定命令 -- ❌ 审计困难 - ---- - -### Redis 6.0+:ACL(访问控制列表) - -**引入时间:** Redis 6.0 (2020年5月) - -**新特性:** -- ✅ 支持多用户 -- ✅ 每个用户有独立的密码 -- ✅ 细粒度权限控制(命令、key、channel) -- ✅ 默认用户 `default` 兼容旧版本 -- ✅ 支持运行时动态修改 - -**架构对比:** -``` -传统模式: -┌─────────────┐ -│ 所有客户端 │ -└──────┬──────┘ - │ (一个密码) - ↓ -┌─────────────┐ -│ Redis │ -│ (全权限) │ -└─────────────┘ - -ACL 模式: -┌────────┐ ┌────────┐ ┌────────┐ -│ App A │ │ App B │ │ Admin │ -└───┬────┘ └───┬────┘ └───┬────┘ - │ user1 │ user2 │ admin - │ (读写) │ (只读) │ (全部) - ↓ ↓ ↓ -┌───────────────────────────────┐ -│ Redis ACL │ -│ user1: +@write +@read │ -│ user2: +@read ~cache:* │ -│ admin: +@all ~* │ -└───────────────────────────────┘ -``` - -**ACL 规则语法:** -``` -user on ~ + - -示例: -user alice on >secret123 ~* +@all # 全部权限 -user bob on >pass456 ~cache:* +get +set # 只能操作 cache:* 的 key -user readonly on >readonly ~* +@read -@write # 只读权限 -``` - ---- - -## 🔍 诊断过程 - -### 步骤 1:确认 Pod 容器名称 - -**目的:** 找到 Redis 容器的正确名称,以便执行命令 - -**命令:** -```bash -kubectl get pod user-redis-0 -n juwan -o jsonpath='{.spec.containers[*].name}' -``` - -**输出:** -``` -user-redis redis-exporter -``` - -**分析:** -- ✅ Pod 中有 2 个容器 -- 📦 `user-redis`:Redis 主容器 -- 📊 `redis-exporter`:Prometheus 监控容器 -- 🎯 **需要连接到 `user-redis` 容器** - -**关键信息:** -```yaml -Pod: user-redis-0 -Containers: - - name: user-redis # ← Redis 服务容器 - image: quay.io/opstree/redis:v7.0.12 - port: 6379 - - name: redis-exporter # ← 监控容器 - image: quay.io/opstree/redis-exporter - port: 9121 -``` - ---- - -### 步骤 2:获取 Redis 密码 - -**目的:** 从 Kubernetes Secret 中提取密码,用于认证 - -**命令:** -```bash -kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d -``` - -**输出:** -``` -<密码内容> # 实际密码已隐藏 -``` - -**说明:** -- Secret 中的数据以 base64 编码存储 -- 需要解码才能得到明文密码 -- `base64 -d` 在 Linux/Mac 上;Windows PowerShell 需要用其他方法 - -**Windows PowerShell 解码方法:** -```powershell -$encoded = kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' -[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded)) -``` - ---- - -### 步骤 3:检查 ACL 配置(关键步骤) - -**目的:** 查询 Redis 的用户配置,确认是否启用 ACL 以及有哪些用户 - -**命令:** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a $(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) \ - ACL LIST -``` - -**命令解析:** -```bash -kubectl exec -it user-redis-0 \ # 在 user-redis-0 Pod 中执行命令 - -n juwan \ # 命名空间 - -c user-redis \ # 指定容器 - -- \ # 分隔符 - redis-cli \ # Redis 命令行工具 - -a $(kubectl get secret ...) \ # 使用密码认证 - ACL LIST # 列出所有 ACL 用户 -``` - -**输出:** -``` -Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. -1) "user default on #e3e7d4b9413497efc274c747b2ee88023e00e6416080db92c7bdd49a73f32d3d ~* &* +@all" -``` - -**输出分析:** - -#### 警告信息 -``` -Warning: Using a password with '-a' option on the command line interface may not be safe. -``` -- ⚠️ 安全提醒:命令行中的密码可能被历史记录或进程列表泄露 -- 💡 生产环境建议:使用配置文件或环境变量传递密码 - -#### ACL 规则详解 -``` -user default on #e3e7d4b...73f32d3d ~* &* +@all -| | | | | | | -| | | | | | └─ 权限:所有命令 -| | | | | └──── 所有 Pub/Sub channel -| | | | └─────── 所有 key pattern -| | | └──────────────────────────── 密码哈希(SHA256) -| | └─────────────────────────────── 状态:已启用 -| └─────────────────────────────────────── 用户名 -└──────────────────────────────────────────── 类型:用户 -``` - -**权限标识含义:** - -| 标识 | 含义 | 说明 | -|-----|------|------| -| `on` | 用户已启用 | 可以登录 | -| `off` | 用户已禁用 | 无法登录 | -| `~*` | Key Pattern | `*` = 所有 key;`~cache:*` = 只能访问 cache 开头的 key | -| `&*` | Pub/Sub Pattern | `*` = 所有 channel | -| `+@all` | 命令权限 | `@all` = 所有命令;`+get +set` = 只能用 GET/SET | -| `-@dangerous` | 禁止命令组 | 禁止危险命令(FLUSHDB, KEYS 等)| -| `#hash` | 密码哈希 | SHA256 哈希值,不存储明文 | - -**命令组示例:** -- `@read`:只读命令(GET, HGET, LRANGE 等) -- `@write`:写入命令(SET, HSET, LPUSH 等) -- `@admin`:管理命令(CONFIG, SHUTDOWN 等) -- `@dangerous`:危险命令(FLUSHALL, KEYS 等) -- `@all`:所有命令 - -**结论:** -- ✅ Redis 启用了 ACL 模式 -- 👤 **用户名是:`default`** -- 🔑 密码已配置(存储为 SHA256 哈希) -- 🔓 权限:完全访问(所有命令、所有 key、所有 channel) - ---- - -### 步骤 4:验证用户名配置 - -**目的:** 确认 `default` 用户可以正常工作 - -**方法 1:使用密码(不指定用户名)** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a PING -``` - -**预期输出:** -``` -Warning: Using a password with '-a' option on the command line interface may not be safe. -PONG -``` - -✅ **说明:只提供密码可以正常连接(默认使用 default 用户)** - ---- - -**方法 2:显式指定用户名** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli --user default --pass PING -``` - -**预期输出:** -``` -PONG -``` - -✅ **说明:显式指定 default 用户也可以正常连接** - ---- - -**方法 3:测试错误的用户名** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli --user wronguser --pass PING -``` - -**预期输出:** -``` -(error) WRONGPASS invalid username-password pair or user is disabled. -``` - -❌ **说明:不存在的用户名会认证失败** - ---- - -### 步骤 5:查看完整的 ACL 信息 - -**目的:** 了解 `default` 用户的详细权限配置 - -**命令:** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL GETUSER default -``` - -**输出:** -``` - 1) "flags" - 2) 1) "on" - 2) "allkeys" - 3) "allchannels" - 4) "allcommands" - 3) "passwords" - 4) 1) "e3e7d4b9413497efc274c747b2ee88023e00e6416080db92c7bdd49a73f32d3d" - 5) "commands" - 6) "+@all" - 7) "keys" - 8) 1) "*" - 9) "channels" -10) 1) "*" -11) "selectors" -12) (empty array) -``` - -**详细分析:** - -| 字段 | 值 | 含义 | -|-----|---|------| -| `flags` | `on, allkeys, allchannels, allcommands` | 用户已启用,可访问所有资源 | -| `passwords` | `e3e7d4b...` (SHA256) | 密码哈希 | -| `commands` | `+@all` | 允许所有命令组 | -| `keys` | `*` | 可访问所有 key | -| `channels` | `*` | 可访问所有 Pub/Sub channel | - -**权限总结:** -- ✅ 用户状态:启用(on) -- ✅ 命令权限:所有命令(+@all) -- ✅ Key 权限:所有 key(~*) -- ✅ Channel 权限:所有 channel(&*) -- 🔒 密码:已设置且加密存储 - ---- - -## 🎯 用户名发现结果 - -### 最终结论 - -#### ✅ 当前 Redis 配置 -```yaml -认证模式: ACL (Redis 6.0+) -用户名: default -密码: (存储在 Secret user-redis 中) -权限: 完全访问(超级用户) -状态: 已启用 -``` - -#### 📝 连接参数汇总 - -| 参数 | 值 | 备注 | -|-----|---|------| -| **用户名** | `default` | 可省略,客户端默认使用 | -| **密码** | `kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' \| base64 -d` | 必需 | -| **Host (集群内)** | `user-redis-master.juwan.svc.cluster.local` | 写操作 | -| **Host (集群内)** | `user-redis-replica.juwan.svc.cluster.local` | 读操作 | -| **Host (Sentinel)** | `user-redis-sentinel-sentinel.juwan.svc.cluster.local` | 推荐 | -| **Port** | `6379` | Redis 数据端口 | -| **Sentinel Port** | `26379` | Sentinel 端口 | - ---- - -### 为什么大多数情况不需要指定用户名? - -#### Redis 客户端的默认行为 - -1. **向后兼容** - ``` - Redis 6.0+ 保持了与旧版本的兼容性 - 当客户端只提供密码时,自动使用 'default' 用户 - ``` - -2. **客户端实现** - ```go - // go-redis 内部逻辑(伪代码) - if password != "" && username == "" { - username = "default" // 自动补全 - } - ``` - -3. **AUTH 命令演进** - ```bash - # Redis 5.x 及之前 - AUTH password # 只有密码 - - # Redis 6.0+(向后兼容) - AUTH password # 等价于 AUTH default password - AUTH username password # 新格式 - ``` - -#### 何时需要显式指定用户名? - -| 场景 | 是否需要 | 原因 | -|-----|---------|------| -| 使用 `default` 用户 | ❌ 不需要 | 客户端自动使用 | -| 创建了自定义用户 | ✅ 需要 | 必须明确指定 | -| 多应用共享 Redis | ✅ 推荐 | 权限隔离 | -| 审计需求 | ✅ 推荐 | 区分访问来源 | -| 管理工具连接 | ⚠️ 可选 | 有些工具要求填写 | - ---- - -## 💻 各语言连接配置 - -### Go (go-redis) - -#### 方式 1:只用密码(推荐) -```go -import "github.com/redis/go-redis/v9" - -// 不指定 Username,自动使用 default -rdb := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Password: os.Getenv("REDIS_PASSWORD"), // 只需密码 - DB: 0, -}) -``` - -#### 方式 2:显式指定用户名 -```go -// 显式指定 Username(效果相同) -rdb := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Username: "default", // 明确指定 - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, -}) -``` - -#### Sentinel 模式(生产推荐) -```go -// Sentinel 模式也支持用户名 -rdb := redis.NewFailoverClient(&redis.FailoverOptions{ - MasterName: "mymaster", - SentinelAddrs: []string{ - "user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379", - }, - // Username 可省略(默认 default) - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, -}) -``` - ---- - -### Go-Zero 框架 - -#### 配置文件 -```yaml -# app/users/rpc/etc/pb.yaml -Redis: - Type: sentinel - MasterName: mymaster - SentinelAddrs: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Pass: ${REDIS_PASSWORD} # 只需密码,不需要用户名 -``` - -#### 如果需要自定义用户 -```yaml -Redis: - Type: sentinel - MasterName: mymaster - SentinelAddrs: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Username: default # 可选:显式指定 - Pass: ${REDIS_PASSWORD} -``` - ---- - -### Python (redis-py) - -#### 只用密码 -```python -import redis - -# 不指定 username -r = redis.Redis( - host='user-redis-master.juwan.svc.cluster.local', - port=6379, - password=os.getenv('REDIS_PASSWORD'), # 只需密码 - db=0 -) -``` - -#### 显式指定用户名(redis-py 4.3+) -```python -# 显式指定 username -r = redis.Redis( - host='user-redis-master.juwan.svc.cluster.local', - port=6379, - username='default', # 明确指定 - password=os.getenv('REDIS_PASSWORD'), - db=0 -) -``` - -#### Sentinel 模式 -```python -from redis.sentinel import Sentinel - -sentinel = Sentinel([ - ('user-redis-sentinel-sentinel.juwan.svc.cluster.local', 26379) -]) - -# 获取主节点(不指定 username) -master = sentinel.master_for( - 'mymaster', - password=os.getenv('REDIS_PASSWORD') -) - -# 或显式指定 -master = sentinel.master_for( - 'mymaster', - username='default', - password=os.getenv('REDIS_PASSWORD') -) -``` - ---- - -### Java (Spring Data Redis) - -#### application.yml(只用密码) -```yaml -spring: - redis: - password: ${REDIS_PASSWORD} # 只需密码 - sentinel: - master: mymaster - nodes: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 -``` - -#### 显式指定用户名(Spring Boot 2.6+) -```yaml -spring: - redis: - username: default # 可选 - password: ${REDIS_PASSWORD} - sentinel: - master: mymaster - nodes: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 -``` - -#### Java 代码(Jedis) -```java -// 只用密码 -JedisPoolConfig poolConfig = new JedisPoolConfig(); -JedisPool pool = new JedisPool( - poolConfig, - "user-redis-master.juwan.svc.cluster.local", - 6379, - 2000, - "password" // 只需密码 -); - -// 显式指定用户名(Jedis 4.0+) -JedisPool pool = new JedisPool( - poolConfig, - "user-redis-master.juwan.svc.cluster.local", - 6379, - 2000, - "default", // 用户名 - "password" // 密码 -); -``` - ---- - -### Node.js (ioredis) - -#### 只用密码 -```javascript -const Redis = require('ioredis'); - -// 不指定 username -const redis = new Redis({ - host: 'user-redis-master.juwan.svc.cluster.local', - port: 6379, - password: process.env.REDIS_PASSWORD, // 只需密码 -}); -``` - -#### 显式指定用户名(ioredis 4.0+) -```javascript -// 显式指定 username -const redis = new Redis({ - host: 'user-redis-master.juwan.svc.cluster.local', - port: 6379, - username: 'default', // 明确指定 - password: process.env.REDIS_PASSWORD, -}); -``` - -#### Sentinel 模式 -```javascript -const redis = new Redis({ - sentinels: [ - { - host: 'user-redis-sentinel-sentinel.juwan.svc.cluster.local', - port: 26379 - } - ], - name: 'mymaster', - password: process.env.REDIS_PASSWORD, // username 可省略 -}); -``` - ---- - -### redis-cli 命令行 - -#### 只用密码 -```bash -# 方式 1:命令行参数 -redis-cli -h user-redis-master.juwan.svc.cluster.local -p 6379 -a - -# 方式 2:交互式登录 -redis-cli -h user-redis-master.juwan.svc.cluster.local -p 6379 -> AUTH -OK -``` - -#### 显式指定用户名 -```bash -# 方式 1:命令行参数 -redis-cli -h host -p 6379 --user default --pass - -# 方式 2:交互式登录 -redis-cli -h host -p 6379 -> AUTH default -OK -``` - ---- - -## 🛠️ 管理工具配置 - -### RedisInsight - -**配置示例:** -``` -Name: juwan-user-redis -Host: localhost (使用 kubectl port-forward) -Port: 6379 -Username: default ← 填这个(或留空) -Password: <从 Secret 获取> -``` - -**说明:** -- ✅ Username 可以填 `default` -- ✅ Username 也可以留空(某些版本支持) -- 🔧 建议先 port-forward:`kubectl port-forward -n juwan svc/user-redis-master 6379:6379` - ---- - -### Redis Commander - -**Docker 运行:** -```bash -docker run -d \ - -e REDIS_HOSTS=juwan:user-redis-master.juwan.svc.cluster.local:6379:0:password \ - -p 8081:8081 \ - rediscommander/redis-commander -``` - -**URL 格式:** -``` -# 不带用户名 -redis://:@host:6379 - -# 带用户名 -redis://default:@host:6379 -``` - ---- - -### Another Redis Desktop Manager - -**连接配置:** -```json -{ - "name": "juwan-redis", - "host": "localhost", - "port": 6379, - "auth": "password", // 只需密码 - "username": "" // 留空(或填 "default") -} -``` - ---- - -## ❓ 常见问题解答 - -### Q1: 为什么有些地方说 Redis 不需要用户名? - -**A:** 这取决于 Redis 版本: - -``` -Redis 5.x 及之前: -└─ ❌ 不支持用户名,只有全局密码 - └─ 配置:requirepass password - -Redis 6.0+: -└─ ✅ 支持 ACL 多用户 - └─ 但保持向后兼容 - └─ 如果只提供密码,自动使用 'default' 用户 -``` - -### Q2: 我的 Redis 是哪个版本? - -**查看方法:** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- redis-cli -v -# 或 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- redis-server --version -``` - -**你的环境:** -``` -Redis 7.0.12 (quay.io/opstree/redis:v7.0.12) -✅ 支持 ACL -✅ 用户名: default -``` - -### Q3: 如何创建自定义用户? - -**场景:** 为不同应用创建独立的用户账号 - -#### 创建只读用户 -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER readonly on \ - '>readonly_password' \ - ~* \ - +@read -@write -@dangerous -``` - -**验证:** -```bash -# 使用新用户登录 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli --user readonly --pass readonly_password GET somekey - -# 尝试写操作(应该失败) -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli --user readonly --pass readonly_password SET somekey value -# (error) NOPERM User has no permissions to run the 'set' command -``` - -#### 创建应用专用用户 -```bash -# 只能访问 app:* 开头的 key -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER app_user on \ - '>app_password' \ - ~app:* \ - +@all -``` - -#### 保存 ACL 配置 -```bash -# 持久化到配置文件 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SAVE -``` - -⚠️ **注意:** 在 Kubernetes 环境中,Pod 重启可能丢失 ACL 配置。建议: -1. 使用 ConfigMap 存储 ACL 配置 -2. 在 StatefulSet 启动脚本中加载配置 -3. 或使用 Redis Operator 的 ACL 管理功能 - -### Q4: 如何查看所有用户? - -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL LIST -``` - -### Q5: 如何重置用户密码? - -#### 重置 default 用户密码 -```bash -# 1. 生成新密码 -NEW_PASSWORD=$(openssl rand -base64 32) - -# 2. 在 Redis 中更新 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER default ">$NEW_PASSWORD" - -# 3. 更新 Kubernetes Secret -kubectl create secret generic user-redis \ - --from-literal=password=$NEW_PASSWORD \ - --dry-run=client -o yaml | kubectl apply -f - - -# 4. 重启应用 Pods(使新密码生效) -kubectl rollout restart deployment/user-rpc -n juwan -``` - -### Q6: 客户端报错 "WRONGPASS invalid username-password pair" - -**可能原因:** - -1. **密码错误** - ```bash - # 验证密码 - kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d - ``` - -2. **用户名错误** - ```bash - # 检查用户是否存在 - kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL LIST - ``` - -3. **用户被禁用** - ```bash - # 启用用户 - kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER default on - ``` - -4. **网络连接到了错误的 Redis 实例** - ```bash - # 确认连接的主机 - kubectl get svc -n juwan | grep redis - ``` - -### Q7: 在 Kubernetes 中如何安全地传递密码? - -**推荐方案:环境变量 + Secret** - -```yaml -# Deployment -apiVersion: apps/v1 -kind: Deployment -metadata: - name: user-rpc -spec: - template: - spec: - containers: - - name: user-rpc - env: - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: user-redis - key: password -``` - -**应用代码:** -```go -// 从环境变量读取 -password := os.Getenv("REDIS_PASSWORD") - -rdb := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Password: password, -}) -``` - -**❌ 不推荐:** -```yaml -# 不要在配置文件中硬编码密码 -Redis: - Password: "hardcoded_password" # ❌ 不安全 -``` - ---- - -## 🔒 安全建议 - -### 1. 密码强度 - -**检查当前密码强度:** -```bash -PASSWORD=$(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) -echo "密码长度: ${#PASSWORD}" -``` - -**推荐:** -- ✅ 至少 32 字符 -- ✅ 包含大小写字母、数字、特殊字符 -- ✅ 使用密码生成器:`openssl rand -base64 32` - -### 2. 权限最小化 - -**不要所有应用都用 default 超级用户!** - -```bash -# 为不同应用创建独立用户 -# 用户 A:只能读写自己的 key -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER app_a on \ - '>password_a' ~app_a:* +@all - -# 用户 B:只读 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER app_b on \ - '>password_b' ~* +@read -``` - -### 3. 禁止危险命令 - -**即使是 default 用户,也应该限制危险命令:** - -```bash -# 禁止 FLUSHALL, FLUSHDB, KEYS 等命令 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER default -flushall -flushdb -keys - -# 查看限制后的权限 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL GETUSER default -``` - -### 4. 审计日志 - -**启用 ACL 日志记录:** -```bash -# 查看最近的 ACL 事件 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL LOG 10 - -# 输出示例: -# 1) reason: auth -# username: default -# timestamp: 1234567890 -``` - -### 5. 定期轮换密码 - -**建议:每 90 天轮换一次** - -```bash -#!/bin/bash -# rotate-redis-password.sh - -# 1. 生成新密码 -NEW_PWD=$(openssl rand -base64 32) - -# 2. 更新 Redis -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a "$OLD_PWD" ACL SETUSER default ">$NEW_PWD" - -# 3. 更新 Secret -kubectl create secret generic user-redis \ - --from-literal=password="$NEW_PWD" \ - -n juwan --dry-run=client -o yaml | kubectl apply -f - - -# 4. 滚动重启应用 -kubectl rollout restart deployment/user-rpc -n juwan - -echo "密码已轮换,新密码: $NEW_PWD" -``` - -### 6. 网络隔离 - -**NetworkPolicy 限制访问:** -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: redis-access-policy - namespace: juwan -spec: - podSelector: - matchLabels: - app: user-redis - policyTypes: - - Ingress - ingress: - # 只允许 user-rpc 访问 - - from: - - podSelector: - matchLabels: - app: user-rpc - ports: - - protocol: TCP - port: 6379 -``` - ---- - -## 📝 总结 - -### 核心发现 - -| 项目 | 值 | -|-----|---| -| 认证模式 | ✅ ACL (Redis 6.0+) | -| 用户名 | `default` | -| 密码位置 | Secret `user-redis` in namespace `juwan` | -| 权限级别 | 超级用户(+@all ~* &*) | -| 是否必须指定用户名 | ❌ 不必须(客户端默认使用 default) | - -### 最佳实践 - -1. **开发/测试环境** - ```yaml - # 简单配置即可 - Password: - # Username 省略 - ``` - -2. **生产环境(推荐)** - ```yaml - # 为每个应用创建独立用户 - Username: app_user - Password: - # 限制权限和 key 范围 - ``` - -3. **连接方式** - ``` - ⭐⭐⭐ Sentinel 模式(推荐) - - 自动故障转移 - - 高可用 - - 只需密码(username 可省略) - ``` - ---- - -**文档版本:** 1.0 -**创建日期:** 2026年2月22日 -**维护者:** DevOps Team -**下次审查:** 2026年3月22日 +# Redis 用户名查找和认证配置指南 + +**问题日期:** 2026年2月22日 +**环境:** juwan namespace +**Redis 版本:** 7.0.12 +**部署模式:** RedisReplication (OpsTree Operator) + +--- + +## 📋 目录 + +1. [问题背景](#问题背景) +2. [Redis 认证机制演进](#redis-认证机制演进) +3. [诊断过程](#诊断过程) +4. [用户名发现结果](#用户名发现结果) +5. [各语言连接配置](#各语言连接配置) +6. [常见问题解答](#常见问题解答) +7. [安全建议](#安全建议) + +--- + +## 🎯 问题背景 + +### 问题描述 +在配置应用连接 Redis 时,发现只有密码信息(存储在 Kubernetes Secret `user-redis` 中),但不清楚: +- Redis 是否需要用户名? +- 如果需要,用户名是什么? +- 如何查找 Redis 的用户名配置? + +### 初始已知信息 +```yaml +Secret: user-redis +Namespace: juwan +Key: password +Value: (base64 编码的密码) +``` + +### 疑问 +- 使用 RedisInsight 等管理工具时,Username 字段应该填什么? +- 应用代码中是否需要配置 Username? +- 不同 Redis 版本的认证方式是否有差异? + +--- + +## 📚 Redis 认证机制演进 + +### Redis 6.0 之前:传统密码认证 + +**特点:** +- ❌ 不支持多用户 +- ✅ 只需配置一个全局密码 +- 🔧 配置项:`requirepass` +- 👤 隐式用户名:`default`(客户端不需要指定) + +**配置示例:** +```conf +# redis.conf +requirepass mypassword +``` + +**连接方式:** +```bash +# 只需要密码 +redis-cli -h host -p 6379 -a mypassword + +# 或使用 AUTH 命令 +redis-cli -h host -p 6379 +> AUTH mypassword +``` + +**优点:** +- ✅ 配置简单 +- ✅ 向后兼容 + +**缺点:** +- ❌ 所有客户端共享同一密码 +- ❌ 无法区分不同应用的访问权限 +- ❌ 无法限制特定命令 +- ❌ 审计困难 + +--- + +### Redis 6.0+:ACL(访问控制列表) + +**引入时间:** Redis 6.0 (2020年5月) + +**新特性:** +- ✅ 支持多用户 +- ✅ 每个用户有独立的密码 +- ✅ 细粒度权限控制(命令、key、channel) +- ✅ 默认用户 `default` 兼容旧版本 +- ✅ 支持运行时动态修改 + +**架构对比:** +``` +传统模式: +┌─────────────┐ +│ 所有客户端 │ +└──────┬──────┘ + │ (一个密码) + ↓ +┌─────────────┐ +│ Redis │ +│ (全权限) │ +└─────────────┘ + +ACL 模式: +┌────────┐ ┌────────┐ ┌────────┐ +│ App A │ │ App B │ │ Admin │ +└───┬────┘ └───┬────┘ └───┬────┘ + │ user1 │ user2 │ admin + │ (读写) │ (只读) │ (全部) + ↓ ↓ ↓ +┌───────────────────────────────┐ +│ Redis ACL │ +│ user1: +@write +@read │ +│ user2: +@read ~cache:* │ +│ admin: +@all ~* │ +└───────────────────────────────┘ +``` + +**ACL 规则语法:** +``` +user on ~ + + +示例: +user alice on >secret123 ~* +@all # 全部权限 +user bob on >pass456 ~cache:* +get +set # 只能操作 cache:* 的 key +user readonly on >readonly ~* +@read -@write # 只读权限 +``` + +--- + +## 🔍 诊断过程 + +### 步骤 1:确认 Pod 容器名称 + +**目的:** 找到 Redis 容器的正确名称,以便执行命令 + +**命令:** +```bash +kubectl get pod user-redis-0 -n juwan -o jsonpath='{.spec.containers[*].name}' +``` + +**输出:** +``` +user-redis redis-exporter +``` + +**分析:** +- ✅ Pod 中有 2 个容器 +- 📦 `user-redis`:Redis 主容器 +- 📊 `redis-exporter`:Prometheus 监控容器 +- 🎯 **需要连接到 `user-redis` 容器** + +**关键信息:** +```yaml +Pod: user-redis-0 +Containers: + - name: user-redis # ← Redis 服务容器 + image: quay.io/opstree/redis:v7.0.12 + port: 6379 + - name: redis-exporter # ← 监控容器 + image: quay.io/opstree/redis-exporter + port: 9121 +``` + +--- + +### 步骤 2:获取 Redis 密码 + +**目的:** 从 Kubernetes Secret 中提取密码,用于认证 + +**命令:** +```bash +kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d +``` + +**输出:** +``` +<密码内容> # 实际密码已隐藏 +``` + +**说明:** +- Secret 中的数据以 base64 编码存储 +- 需要解码才能得到明文密码 +- `base64 -d` 在 Linux/Mac 上;Windows PowerShell 需要用其他方法 + +**Windows PowerShell 解码方法:** +```powershell +$encoded = kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' +[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded)) +``` + +--- + +### 步骤 3:检查 ACL 配置(关键步骤) + +**目的:** 查询 Redis 的用户配置,确认是否启用 ACL 以及有哪些用户 + +**命令:** +```bash +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a $(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) \ + ACL LIST +``` + +**命令解析:** +```bash +kubectl exec -it user-redis-0 \ # 在 user-redis-0 Pod 中执行命令 + -n juwan \ # 命名空间 + -c user-redis \ # 指定容器 + -- \ # 分隔符 + redis-cli \ # Redis 命令行工具 + -a $(kubectl get secret ...) \ # 使用密码认证 + ACL LIST # 列出所有 ACL 用户 +``` + +**输出:** +``` +Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. +1) "user default on #e3e7d4b9413497efc274c747b2ee88023e00e6416080db92c7bdd49a73f32d3d ~* &* +@all" +``` + +**输出分析:** + +#### 警告信息 +``` +Warning: Using a password with '-a' option on the command line interface may not be safe. +``` +- ⚠️ 安全提醒:命令行中的密码可能被历史记录或进程列表泄露 +- 💡 生产环境建议:使用配置文件或环境变量传递密码 + +#### ACL 规则详解 +``` +user default on #e3e7d4b...73f32d3d ~* &* +@all +| | | | | | | +| | | | | | └─ 权限:所有命令 +| | | | | └──── 所有 Pub/Sub channel +| | | | └─────── 所有 key pattern +| | | └──────────────────────────── 密码哈希(SHA256) +| | └─────────────────────────────── 状态:已启用 +| └─────────────────────────────────────── 用户名 +└──────────────────────────────────────────── 类型:用户 +``` + +**权限标识含义:** + +| 标识 | 含义 | 说明 | +|-----|------|------| +| `on` | 用户已启用 | 可以登录 | +| `off` | 用户已禁用 | 无法登录 | +| `~*` | Key Pattern | `*` = 所有 key;`~cache:*` = 只能访问 cache 开头的 key | +| `&*` | Pub/Sub Pattern | `*` = 所有 channel | +| `+@all` | 命令权限 | `@all` = 所有命令;`+get +set` = 只能用 GET/SET | +| `-@dangerous` | 禁止命令组 | 禁止危险命令(FLUSHDB, KEYS 等)| +| `#hash` | 密码哈希 | SHA256 哈希值,不存储明文 | + +**命令组示例:** +- `@read`:只读命令(GET, HGET, LRANGE 等) +- `@write`:写入命令(SET, HSET, LPUSH 等) +- `@admin`:管理命令(CONFIG, SHUTDOWN 等) +- `@dangerous`:危险命令(FLUSHALL, KEYS 等) +- `@all`:所有命令 + +**结论:** +- ✅ Redis 启用了 ACL 模式 +- 👤 **用户名是:`default`** +- 🔑 密码已配置(存储为 SHA256 哈希) +- 🔓 权限:完全访问(所有命令、所有 key、所有 channel) + +--- + +### 步骤 4:验证用户名配置 + +**目的:** 确认 `default` 用户可以正常工作 + +**方法 1:使用密码(不指定用户名)** +```bash +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a PING +``` + +**预期输出:** +``` +Warning: Using a password with '-a' option on the command line interface may not be safe. +PONG +``` + +✅ **说明:只提供密码可以正常连接(默认使用 default 用户)** + +--- + +**方法 2:显式指定用户名** +```bash +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli --user default --pass PING +``` + +**预期输出:** +``` +PONG +``` + +✅ **说明:显式指定 default 用户也可以正常连接** + +--- + +**方法 3:测试错误的用户名** +```bash +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli --user wronguser --pass PING +``` + +**预期输出:** +``` +(error) WRONGPASS invalid username-password pair or user is disabled. +``` + +❌ **说明:不存在的用户名会认证失败** + +--- + +### 步骤 5:查看完整的 ACL 信息 + +**目的:** 了解 `default` 用户的详细权限配置 + +**命令:** +```bash +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL GETUSER default +``` + +**输出:** +``` + 1) "flags" + 2) 1) "on" + 2) "allkeys" + 3) "allchannels" + 4) "allcommands" + 3) "passwords" + 4) 1) "e3e7d4b9413497efc274c747b2ee88023e00e6416080db92c7bdd49a73f32d3d" + 5) "commands" + 6) "+@all" + 7) "keys" + 8) 1) "*" + 9) "channels" +10) 1) "*" +11) "selectors" +12) (empty array) +``` + +**详细分析:** + +| 字段 | 值 | 含义 | +|-----|---|------| +| `flags` | `on, allkeys, allchannels, allcommands` | 用户已启用,可访问所有资源 | +| `passwords` | `e3e7d4b...` (SHA256) | 密码哈希 | +| `commands` | `+@all` | 允许所有命令组 | +| `keys` | `*` | 可访问所有 key | +| `channels` | `*` | 可访问所有 Pub/Sub channel | + +**权限总结:** +- ✅ 用户状态:启用(on) +- ✅ 命令权限:所有命令(+@all) +- ✅ Key 权限:所有 key(~*) +- ✅ Channel 权限:所有 channel(&*) +- 🔒 密码:已设置且加密存储 + +--- + +## 🎯 用户名发现结果 + +### 最终结论 + +#### ✅ 当前 Redis 配置 +```yaml +认证模式: ACL (Redis 6.0+) +用户名: default +密码: (存储在 Secret user-redis 中) +权限: 完全访问(超级用户) +状态: 已启用 +``` + +#### 📝 连接参数汇总 + +| 参数 | 值 | 备注 | +|-----|---|------| +| **用户名** | `default` | 可省略,客户端默认使用 | +| **密码** | `kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' \| base64 -d` | 必需 | +| **Host (集群内)** | `user-redis-master.juwan.svc.cluster.local` | 写操作 | +| **Host (集群内)** | `user-redis-replica.juwan.svc.cluster.local` | 读操作 | +| **Host (Sentinel)** | `user-redis-sentinel-sentinel.juwan.svc.cluster.local` | 推荐 | +| **Port** | `6379` | Redis 数据端口 | +| **Sentinel Port** | `26379` | Sentinel 端口 | + +--- + +### 为什么大多数情况不需要指定用户名? + +#### Redis 客户端的默认行为 + +1. **向后兼容** + ``` + Redis 6.0+ 保持了与旧版本的兼容性 + 当客户端只提供密码时,自动使用 'default' 用户 + ``` + +2. **客户端实现** + ```go + // go-redis 内部逻辑(伪代码) + if password != "" && username == "" { + username = "default" // 自动补全 + } + ``` + +3. **AUTH 命令演进** + ```bash + # Redis 5.x 及之前 + AUTH password # 只有密码 + + # Redis 6.0+(向后兼容) + AUTH password # 等价于 AUTH default password + AUTH username password # 新格式 + ``` + +#### 何时需要显式指定用户名? + +| 场景 | 是否需要 | 原因 | +|-----|---------|------| +| 使用 `default` 用户 | ❌ 不需要 | 客户端自动使用 | +| 创建了自定义用户 | ✅ 需要 | 必须明确指定 | +| 多应用共享 Redis | ✅ 推荐 | 权限隔离 | +| 审计需求 | ✅ 推荐 | 区分访问来源 | +| 管理工具连接 | ⚠️ 可选 | 有些工具要求填写 | + +--- + +## 💻 各语言连接配置 + +### Go (go-redis) + +#### 方式 1:只用密码(推荐) +```go +import "github.com/redis/go-redis/v9" + +// 不指定 Username,自动使用 default +rdb := redis.NewClient(&redis.Options{ + Addr: "user-redis-master.juwan.svc.cluster.local:6379", + Password: os.Getenv("REDIS_PASSWORD"), // 只需密码 + DB: 0, +}) +``` + +#### 方式 2:显式指定用户名 +```go +// 显式指定 Username(效果相同) +rdb := redis.NewClient(&redis.Options{ + Addr: "user-redis-master.juwan.svc.cluster.local:6379", + Username: "default", // 明确指定 + Password: os.Getenv("REDIS_PASSWORD"), + DB: 0, +}) +``` + +#### Sentinel 模式(生产推荐) +```go +// Sentinel 模式也支持用户名 +rdb := redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: "mymaster", + SentinelAddrs: []string{ + "user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379", + }, + // Username 可省略(默认 default) + Password: os.Getenv("REDIS_PASSWORD"), + DB: 0, +}) +``` + +--- + +### Go-Zero 框架 + +#### 配置文件 +```yaml +# app/users/rpc/etc/pb.yaml +Redis: + Type: sentinel + MasterName: mymaster + SentinelAddrs: + - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Pass: ${REDIS_PASSWORD} # 只需密码,不需要用户名 +``` + +#### 如果需要自定义用户 +```yaml +Redis: + Type: sentinel + MasterName: mymaster + SentinelAddrs: + - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 + Username: default # 可选:显式指定 + Pass: ${REDIS_PASSWORD} +``` + +--- + +### Python (redis-py) + +#### 只用密码 +```python +import redis + +# 不指定 username +r = redis.Redis( + host='user-redis-master.juwan.svc.cluster.local', + port=6379, + password=os.getenv('REDIS_PASSWORD'), # 只需密码 + db=0 +) +``` + +#### 显式指定用户名(redis-py 4.3+) +```python +# 显式指定 username +r = redis.Redis( + host='user-redis-master.juwan.svc.cluster.local', + port=6379, + username='default', # 明确指定 + password=os.getenv('REDIS_PASSWORD'), + db=0 +) +``` + +#### Sentinel 模式 +```python +from redis.sentinel import Sentinel + +sentinel = Sentinel([ + ('user-redis-sentinel-sentinel.juwan.svc.cluster.local', 26379) +]) + +# 获取主节点(不指定 username) +master = sentinel.master_for( + 'mymaster', + password=os.getenv('REDIS_PASSWORD') +) + +# 或显式指定 +master = sentinel.master_for( + 'mymaster', + username='default', + password=os.getenv('REDIS_PASSWORD') +) +``` + +--- + +### Java (Spring Data Redis) + +#### application.yml(只用密码) +```yaml +spring: + redis: + password: ${REDIS_PASSWORD} # 只需密码 + sentinel: + master: mymaster + nodes: + - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 +``` + +#### 显式指定用户名(Spring Boot 2.6+) +```yaml +spring: + redis: + username: default # 可选 + password: ${REDIS_PASSWORD} + sentinel: + master: mymaster + nodes: + - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 +``` + +#### Java 代码(Jedis) +```java +// 只用密码 +JedisPoolConfig poolConfig = new JedisPoolConfig(); +JedisPool pool = new JedisPool( + poolConfig, + "user-redis-master.juwan.svc.cluster.local", + 6379, + 2000, + "password" // 只需密码 +); + +// 显式指定用户名(Jedis 4.0+) +JedisPool pool = new JedisPool( + poolConfig, + "user-redis-master.juwan.svc.cluster.local", + 6379, + 2000, + "default", // 用户名 + "password" // 密码 +); +``` + +--- + +### Node.js (ioredis) + +#### 只用密码 +```javascript +const Redis = require('ioredis'); + +// 不指定 username +const redis = new Redis({ + host: 'user-redis-master.juwan.svc.cluster.local', + port: 6379, + password: process.env.REDIS_PASSWORD, // 只需密码 +}); +``` + +#### 显式指定用户名(ioredis 4.0+) +```javascript +// 显式指定 username +const redis = new Redis({ + host: 'user-redis-master.juwan.svc.cluster.local', + port: 6379, + username: 'default', // 明确指定 + password: process.env.REDIS_PASSWORD, +}); +``` + +#### Sentinel 模式 +```javascript +const redis = new Redis({ + sentinels: [ + { + host: 'user-redis-sentinel-sentinel.juwan.svc.cluster.local', + port: 26379 + } + ], + name: 'mymaster', + password: process.env.REDIS_PASSWORD, // username 可省略 +}); +``` + +--- + +### redis-cli 命令行 + +#### 只用密码 +```bash +# 方式 1:命令行参数 +redis-cli -h user-redis-master.juwan.svc.cluster.local -p 6379 -a + +# 方式 2:交互式登录 +redis-cli -h user-redis-master.juwan.svc.cluster.local -p 6379 +> AUTH +OK +``` + +#### 显式指定用户名 +```bash +# 方式 1:命令行参数 +redis-cli -h host -p 6379 --user default --pass + +# 方式 2:交互式登录 +redis-cli -h host -p 6379 +> AUTH default +OK +``` + +--- + +## 🛠️ 管理工具配置 + +### RedisInsight + +**配置示例:** +``` +Name: juwan-user-redis +Host: localhost (使用 kubectl port-forward) +Port: 6379 +Username: default ← 填这个(或留空) +Password: <从 Secret 获取> +``` + +**说明:** +- ✅ Username 可以填 `default` +- ✅ Username 也可以留空(某些版本支持) +- 🔧 建议先 port-forward:`kubectl port-forward -n juwan svc/user-redis-master 6379:6379` + +--- + +### Redis Commander + +**Docker 运行:** +```bash +docker run -d \ + -e REDIS_HOSTS=juwan:user-redis-master.juwan.svc.cluster.local:6379:0:password \ + -p 8081:8081 \ + rediscommander/redis-commander +``` + +**URL 格式:** +``` +# 不带用户名 +redis://:@host:6379 + +# 带用户名 +redis://default:@host:6379 +``` + +--- + +### Another Redis Desktop Manager + +**连接配置:** +```json +{ + "name": "juwan-redis", + "host": "localhost", + "port": 6379, + "auth": "password", // 只需密码 + "username": "" // 留空(或填 "default") +} +``` + +--- + +## ❓ 常见问题解答 + +### Q1: 为什么有些地方说 Redis 不需要用户名? + +**A:** 这取决于 Redis 版本: + +``` +Redis 5.x 及之前: +└─ ❌ 不支持用户名,只有全局密码 + └─ 配置:requirepass password + +Redis 6.0+: +└─ ✅ 支持 ACL 多用户 + └─ 但保持向后兼容 + └─ 如果只提供密码,自动使用 'default' 用户 +``` + +### Q2: 我的 Redis 是哪个版本? + +**查看方法:** +```bash +kubectl exec -it user-redis-0 -n juwan -c user-redis -- redis-cli -v +# 或 +kubectl exec -it user-redis-0 -n juwan -c user-redis -- redis-server --version +``` + +**你的环境:** +``` +Redis 7.0.12 (quay.io/opstree/redis:v7.0.12) +✅ 支持 ACL +✅ 用户名: default +``` + +### Q3: 如何创建自定义用户? + +**场景:** 为不同应用创建独立的用户账号 + +#### 创建只读用户 +```bash +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL SETUSER readonly on \ + '>readonly_password' \ + ~* \ + +@read -@write -@dangerous +``` + +**验证:** +```bash +# 使用新用户登录 +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli --user readonly --pass readonly_password GET somekey + +# 尝试写操作(应该失败) +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli --user readonly --pass readonly_password SET somekey value +# (error) NOPERM User has no permissions to run the 'set' command +``` + +#### 创建应用专用用户 +```bash +# 只能访问 app:* 开头的 key +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL SETUSER app_user on \ + '>app_password' \ + ~app:* \ + +@all +``` + +#### 保存 ACL 配置 +```bash +# 持久化到配置文件 +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL SAVE +``` + +⚠️ **注意:** 在 Kubernetes 环境中,Pod 重启可能丢失 ACL 配置。建议: +1. 使用 ConfigMap 存储 ACL 配置 +2. 在 StatefulSet 启动脚本中加载配置 +3. 或使用 Redis Operator 的 ACL 管理功能 + +### Q4: 如何查看所有用户? + +```bash +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL LIST +``` + +### Q5: 如何重置用户密码? + +#### 重置 default 用户密码 +```bash +# 1. 生成新密码 +NEW_PASSWORD=$(openssl rand -base64 32) + +# 2. 在 Redis 中更新 +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL SETUSER default ">$NEW_PASSWORD" + +# 3. 更新 Kubernetes Secret +kubectl create secret generic user-redis \ + --from-literal=password=$NEW_PASSWORD \ + --dry-run=client -o yaml | kubectl apply -f - + +# 4. 重启应用 Pods(使新密码生效) +kubectl rollout restart deployment/user-rpc -n juwan +``` + +### Q6: 客户端报错 "WRONGPASS invalid username-password pair" + +**可能原因:** + +1. **密码错误** + ```bash + # 验证密码 + kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d + ``` + +2. **用户名错误** + ```bash + # 检查用户是否存在 + kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL LIST + ``` + +3. **用户被禁用** + ```bash + # 启用用户 + kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL SETUSER default on + ``` + +4. **网络连接到了错误的 Redis 实例** + ```bash + # 确认连接的主机 + kubectl get svc -n juwan | grep redis + ``` + +### Q7: 在 Kubernetes 中如何安全地传递密码? + +**推荐方案:环境变量 + Secret** + +```yaml +# Deployment +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-rpc +spec: + template: + spec: + containers: + - name: user-rpc + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: user-redis + key: password +``` + +**应用代码:** +```go +// 从环境变量读取 +password := os.Getenv("REDIS_PASSWORD") + +rdb := redis.NewClient(&redis.Options{ + Addr: "user-redis-master.juwan.svc.cluster.local:6379", + Password: password, +}) +``` + +**❌ 不推荐:** +```yaml +# 不要在配置文件中硬编码密码 +Redis: + Password: "hardcoded_password" # ❌ 不安全 +``` + +--- + +## 🔒 安全建议 + +### 1. 密码强度 + +**检查当前密码强度:** +```bash +PASSWORD=$(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) +echo "密码长度: ${#PASSWORD}" +``` + +**推荐:** +- ✅ 至少 32 字符 +- ✅ 包含大小写字母、数字、特殊字符 +- ✅ 使用密码生成器:`openssl rand -base64 32` + +### 2. 权限最小化 + +**不要所有应用都用 default 超级用户!** + +```bash +# 为不同应用创建独立用户 +# 用户 A:只能读写自己的 key +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL SETUSER app_a on \ + '>password_a' ~app_a:* +@all + +# 用户 B:只读 +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL SETUSER app_b on \ + '>password_b' ~* +@read +``` + +### 3. 禁止危险命令 + +**即使是 default 用户,也应该限制危险命令:** + +```bash +# 禁止 FLUSHALL, FLUSHDB, KEYS 等命令 +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL SETUSER default -flushall -flushdb -keys + +# 查看限制后的权限 +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL GETUSER default +``` + +### 4. 审计日志 + +**启用 ACL 日志记录:** +```bash +# 查看最近的 ACL 事件 +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a ACL LOG 10 + +# 输出示例: +# 1) reason: auth +# username: default +# timestamp: 1234567890 +``` + +### 5. 定期轮换密码 + +**建议:每 90 天轮换一次** + +```bash +#!/bin/bash +# rotate-redis-password.sh + +# 1. 生成新密码 +NEW_PWD=$(openssl rand -base64 32) + +# 2. 更新 Redis +kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ + redis-cli -a "$OLD_PWD" ACL SETUSER default ">$NEW_PWD" + +# 3. 更新 Secret +kubectl create secret generic user-redis \ + --from-literal=password="$NEW_PWD" \ + -n juwan --dry-run=client -o yaml | kubectl apply -f - + +# 4. 滚动重启应用 +kubectl rollout restart deployment/user-rpc -n juwan + +echo "密码已轮换,新密码: $NEW_PWD" +``` + +### 6. 网络隔离 + +**NetworkPolicy 限制访问:** +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: redis-access-policy + namespace: juwan +spec: + podSelector: + matchLabels: + app: user-redis + policyTypes: + - Ingress + ingress: + # 只允许 user-rpc 访问 + - from: + - podSelector: + matchLabels: + app: user-rpc + ports: + - protocol: TCP + port: 6379 +``` + +--- + +## 📝 总结 + +### 核心发现 + +| 项目 | 值 | +|-----|---| +| 认证模式 | ✅ ACL (Redis 6.0+) | +| 用户名 | `default` | +| 密码位置 | Secret `user-redis` in namespace `juwan` | +| 权限级别 | 超级用户(+@all ~* &*) | +| 是否必须指定用户名 | ❌ 不必须(客户端默认使用 default) | + +### 最佳实践 + +1. **开发/测试环境** + ```yaml + # 简单配置即可 + Password: + # Username 省略 + ``` + +2. **生产环境(推荐)** + ```yaml + # 为每个应用创建独立用户 + Username: app_user + Password: + # 限制权限和 key 范围 + ``` + +3. **连接方式** + ``` + ⭐⭐⭐ Sentinel 模式(推荐) + - 自动故障转移 + - 高可用 + - 只需密码(username 可省略) + ``` + +--- + +**文档版本:** 1.0 +**创建日期:** 2026年2月22日 +**维护者:** DevOps Team +**下次审查:** 2026年3月22日 diff --git a/docs/secrets/DEPLOYMENT.md b/docs/secrets/DEPLOYMENT.md index 938a60a..188d676 100644 --- a/docs/secrets/DEPLOYMENT.md +++ b/docs/secrets/DEPLOYMENT.md @@ -1,424 +1,424 @@ -# JWT Secret + ETCD Encryption Deployment Guide - -完整的 JWT 认证系统部署指南,包括密钥管理、RBAC 权限控制和 ETCD 加密。 - -## 部署顺序 - -### 第1步:创建 Secret 和 RBAC(必需) - -创建 JWT 秘钥和服务账户权限: - -```bash -kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml -``` - -验证创建成功: - -```bash -# 检查 Secret -kubectl get secret jwt-secret -n juwan -kubectl get secret jwt-secret -n juwan -o yaml - -# 检查 ServiceAccounts -kubectl get sa user-rpc -n juwan -kubectl get sa envoy-gateway -n juwan - -# 检查 RBAC 权限 -kubectl get role jwt-secret-reader -n juwan -kubectl get rolebinding -n juwan -l app=jwt-secret-reader -``` - -### 第2步:更新 user-rpc 部署(依赖第1步) - -已自动更新 `deploy/k8s/service/user/user-rpc.yaml`: - -- ✅ 更新 `serviceAccountName` 从 `find-endpoints` → `user-rpc` -- ✅ 添加环境变量 `JWT_SECRET_KEY` 从 Secret `jwt-secret` 读取 - -应用更新: - -```bash -kubectl apply -f deploy/k8s/service/user/user-rpc.yaml -``` - -验证部署: - -```bash -# 检查 ServiceAccount 已正确绑定 -kubectl get deployment user-rpc -n juwan -o yaml | grep -A 5 serviceAccountName - -# 查看 Pod 是否以 user-rpc ServiceAccount 身份运行 -kubectl get pod -n juwan -l app=user-rpc -o yaml | grep serviceAccount - -# 验证环境变量已注入 -kubectl exec -it POD_NAME -n juwan -- env | grep JWT_SECRET_KEY -``` - -### 第3步:更新 Envoy 网关部署(依赖第1步) - -已自动更新 `deploy/k8s/envoy/envoy.yaml`: - -- ✅ 添加 `serviceAccountName: envoy-gateway` 到 Deployment spec - -应用更新: - -```bash -kubectl apply -f deploy/k8s/envoy/envoy.yaml -``` - -验证部署: - -```bash -# 检查 ServiceAccount 已正确绑定 -kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName - -# 检查 Pod 状态 -kubectl get pod -n juwan -l app=envoy-gateway -``` - -### 第4步:启用 ETCD 加密(强烈推荐用于生产环境) - -这是一个集群级别的配置,需要在 Kubernetes 控制平面节点上执行。 - -**前提条件:** -- 具有 Kubernetes 集群管理员权限 -- 可以访问控制平面节点 -- 备份 ETCD 数据库 - -**步骤:** - -1. **生成加密密钥** - ```bash - head -c 32 /dev/urandom | base64 - ``` - 记录输出的 Base64 密钥。 - -2. **创建加密配置文件** - - 在控制平面节点上,创建 `/etc/kubernetes/encryption-config.yaml`: - - ```yaml - apiVersion: apiserver.config.k8s.io/v1 - kind: EncryptionConfiguration - resources: - - resources: - - secrets - providers: - - aescbc: - keys: - - name: key1 - secret: - - identity: {} - ``` - - 替换 `` 为第1步生成的密钥。 - -3. **修改 kube-apiserver 配置** - - 在控制平面节点上,编辑 `/etc/kubernetes/manifests/kube-apiserver.yaml`: - - **添加参数:** - ```yaml - spec: - containers: - - name: kube-apiserver - command: - - kube-apiserver - - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml - ``` - - **添加卷挂载:** - ```yaml - spec: - containers: - - name: kube-apiserver - volumeMounts: - - name: encryption-config - mountPath: /etc/kubernetes - readOnly: true - - volumes: - - name: encryption-config - hostPath: - path: /etc/kubernetes - type: DirectoryOrCreate - ``` - -4. **重启 kube-apiserver** - - 修改清单后,kubelet 会自动重启 kube-apiserver。检查状态: - - ```bash - # 在控制平面节点 - kubectl get pods -n kube-system | grep kube-apiserver - - # 监控重启过程 - kubectl logs -n kube-system -l component=kube-apiserver -f - ``` - -5. **验证加密是否启用** - - 创建一个新 Secret 并检查它在 ETCD 中是否加密: - - ```bash - # 创建测试 Secret - kubectl create secret generic test-secret -n juwan --from-literal=key=value - - # 从 control plane 节点检查 ETCD 数据 - # 如果数据不可读并包含加密标记,说明加密已启用 - ``` - -6. **保存加密密钥** - - ⚠️ **关键:将加密密钥安全地保存在离线存储中** - - 密钥丢失后,无法解密 ETCD 中的数据 - - 无法恢复任何 Secrets - - 建议用密码管理工具(如 HashiCorp Vault)或 HSM 存储密钥 - -### 第5步:验证整个系统 - -完整的验证清单: - -```bash -# 检查所有 Secrets 已创建 -kubectl get secret -n juwan -kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d - -# 检查 ServiceAccounts 已创建 -kubectl get sa -n juwan -kubectl describe sa user-rpc -n juwan -kubectl describe sa envoy-gateway -n juwan - -# 检查 RBAC 权限 -kubectl get role -n juwan -kubectl get rolebinding -n juwan -kubectl describe role jwt-secret-reader -n juwan - -# 测试权限:user-rpc 可以读 jwt-secret -kubectl auth can-i get secrets --as=system:serviceaccount:juwan:user-rpc --resource-name=jwt-secret -n juwan - -# 测试权限:envoy-gateway 可以读 jwt-secret -kubectl auth can-i get secrets --as=system:serviceaccount:juwan:envoy-gateway --resource-name=jwt-secret -n juwan - -# 测试权限:其他 ServiceAccount 无法读取 -kubectl auth can-i get secrets --as=system:serviceaccount:juwan:other-service -n juwan - -# 检查 Deployments 已正确配置 -kubectl get deployment user-rpc -n juwan -o yaml | grep -A 2 serviceAccountName -kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName - -# 检查 Pods 是否已启动并运行 -kubectl get pods -n juwan -l app=user-rpc -kubectl get pods -n juwan -l app=envoy-gateway - -# 查看 JWT Secret 是否已挂载到 Pod -kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep JWT_SECRET_KEY -``` - -## 监控和日志 - -### 查看 Pod 日志 - -```bash -# user-rpc 日志 -kubectl logs -n juwan -l app=user-rpc -f --all-containers=true - -# Envoy 日志 -kubectl logs -n juwan -l app=envoy-gateway -f -``` - -### 检查 Pod 事件 - -```bash -# 查看 Pod 创建和启动事件 -kubectl describe pod -n juwan -l app=user-rpc -kubectl describe pod -n juwan -l app=envoy-gateway -``` - -### 权限问题排查 - -如果 Pod 无法读取 Secret: - -```bash -# 检查 Pod 使用的 ServiceAccount -kubectl get pod POD_NAME -n juwan -o yaml | grep serviceAccountName - -# 检查 RBAC 绑定 -kubectl get rolebinding -n juwan -o wide - -# 检查 Role 权限定义 -kubectl get role jwt-secret-reader -n juwan -o yaml - -# 尝试用 Pod 的身份读取 Secret(需要 kubectl-user-impersonate 或类似工具) -kubectl get secret jwt-secret --as=system:serviceaccount:juwan:user-rpc -n juwan -``` - -## 安全最佳实践 - -### 1. 密钥轮换 - -周期性更换 JWT 秘钥(建议每季度): - -```bash -# 1. 生成新密钥 -NEW_KEY=$(head -c 32 /dev/urandom | base64) - -# 2. 更新 Secret -kubectl create secret generic jwt-secret \ - --from-literal=secret-key=$NEW_KEY \ - --dry-run=client -o yaml | kubectl apply -f - - -# 3. 重启 Pods 以加载新密钥(滚动更新) -kubectl rollout restart deployment/user-rpc -n juwan -kubectl rollout restart deployment/envoy-gateway -n juwan - -# 4. 验证新 Pods 已启动并运行 -kubectl rollout status deployment/user-rpc -n juwan -kubectl rollout status deployment/envoy-gateway -n juwan - -# 5. 已颁发的旧令牌将变为无效 -# 需要用户重新登录获取新令牌 -``` - -### 2. 审计和监控 - -在生产环境中启用 Kubernetes 审计日志来跟踪 Secret 访问: - -```yaml -# kube-apiserver 审计策略示例 -apiVersion: audit.k8s.io/v1 -kind: Policy -rules: - # 记录 secret 资源的访问 - - level: RequestResponse - verbs: ["get", "list", "watch"] - resources: ["secrets"] - # 记录所有认证失败 - - level: RequestResponse - omitStages: - - RequestReceived - userGroups: ["system:unauthenticated"] - - level: Metadata - omitStages: - - RequestReceived -``` - -### 3. 网络策略 - -使用 NetworkPolicy 限制 Pods 之间的通信: - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: jwt-secret-access - namespace: juwan -spec: - podSelector: - matchLabels: - app: jwt-secret-reader - policyTypes: - - Ingress - ingress: - - from: - - podSelector: - matchLabels: - app: user-rpc - - podSelector: - matchLabels: - app: envoy-gateway - ports: - - protocol: TCP - port: 443 # API Server -``` - -## 灾难恢复 - -### 备份 Secret 和 RBAC 配置 - -```bash -# 备份 JWT Secret -kubectl get secret jwt-secret -n juwan -o yaml > jwt-secret-backup.yaml - -# 备份 RBAC 配置 -kubectl get role jwt-secret-reader -n juwan -o yaml > jwt-role-backup.yaml -kubectl get rolebinding -n juwan -l app=jwt-secret-reader -o yaml > jwt-rolebinding-backup.yaml - -# 加密备份文件 -gpg --symmetric jwt-secret-backup.yaml -``` - -### 恢复步骤 - -如果 Secret 被意外删除: - -```bash -# 从备份恢复 -kubectl apply -f jwt-secret-backup.yaml - -# 重启 Pods 以重新加载 Secret -kubectl rollout restart deployment/user-rpc -n juwan -kubectl rollout restart deployment/envoy-gateway -n juwan -``` - -## 常见问题 - -### Q: Pod 无法启动,显示 "failed to pull secret" - -A: 检查: -1. Secret 是否存在:`kubectl get secret jwt-secret -n juwan` -2. ServiceAccount 是否绑定了 RBAC:`kubectl describe rolebinding -n juwan` -3. Secret 名称和命名空间是否正确 - -### Q: 加密后如何验证 ETCD 中的数据已加密? - -A: 从 control plane 节点: -```bash -# 直接读取 ETCD(如果配置了加密,数据应该不可读) -sudo strings /var/lib/etcd/member/snap/db | grep -i secret -``` - -### Q: 能否更改加密密钥而不重新创建 ETCD? - -A: 可以,但流程复杂: -1. 更新 encryption-config.yaml 中的新密钥 -2. 将新密钥添加到提供程序列表(保持旧密钥) -3. 重启 kube-apiserver -4. 触发重新加密:`kubectl get all --all-namespaces -o json | kubectl apply -f -` - -### Q: 如何在 Minikube 中启用 ETCD 加密? - -A: 参考 `ENCRYPTION.md` 中的 Minikube 特定说明部分。 - -## 相关文件 - -- `jwt-secret.yaml` - Secret 和 RBAC 配置 -- `ENCRYPTION.md` - ETCD 加密详细文档 -- `README.md` - 快速参考指南 -- `/deploy/k8s/service/user/user-rpc.yaml` - user-rpc Deployment 配置 -- `/deploy/k8s/envoy/envoy.yaml` - Envoy 网关 Deployment 配置 - -## 下一步 - -部署完成后: - -1. **集成 JWT 验证到 RPC Handlers** - - 实现 gRPC unary interceptor - - 验证令牌有效性 - - 处理令牌刷新逻辑 - -2. **集成 JWT 验证到 Envoy** - - 扩展 Lua filter 进行令牌验证 - - 返回 401(无效令牌)或 200(有效令牌) - -3. **端到端测试** - - 创建用户和登录 - - 生成和验证 JWT - - 测试令牌刷新和撤销 - - 验证 ETCD 加密 - -4. **生产部署** - - 启用审计日志 - - 配置密钥轮换计划 - - 建立备份和恢复流程 - - 监控 Secret 访问 +# JWT Secret + ETCD Encryption Deployment Guide + +完整的 JWT 认证系统部署指南,包括密钥管理、RBAC 权限控制和 ETCD 加密。 + +## 部署顺序 + +### 第1步:创建 Secret 和 RBAC(必需) + +创建 JWT 秘钥和服务账户权限: + +```bash +kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml +``` + +验证创建成功: + +```bash +# 检查 Secret +kubectl get secret jwt-secret -n juwan +kubectl get secret jwt-secret -n juwan -o yaml + +# 检查 ServiceAccounts +kubectl get sa user-rpc -n juwan +kubectl get sa envoy-gateway -n juwan + +# 检查 RBAC 权限 +kubectl get role jwt-secret-reader -n juwan +kubectl get rolebinding -n juwan -l app=jwt-secret-reader +``` + +### 第2步:更新 user-rpc 部署(依赖第1步) + +已自动更新 `deploy/k8s/service/user/user-rpc.yaml`: + +- ✅ 更新 `serviceAccountName` 从 `find-endpoints` → `user-rpc` +- ✅ 添加环境变量 `JWT_SECRET_KEY` 从 Secret `jwt-secret` 读取 + +应用更新: + +```bash +kubectl apply -f deploy/k8s/service/user/user-rpc.yaml +``` + +验证部署: + +```bash +# 检查 ServiceAccount 已正确绑定 +kubectl get deployment user-rpc -n juwan -o yaml | grep -A 5 serviceAccountName + +# 查看 Pod 是否以 user-rpc ServiceAccount 身份运行 +kubectl get pod -n juwan -l app=user-rpc -o yaml | grep serviceAccount + +# 验证环境变量已注入 +kubectl exec -it POD_NAME -n juwan -- env | grep JWT_SECRET_KEY +``` + +### 第3步:更新 Envoy 网关部署(依赖第1步) + +已自动更新 `deploy/k8s/envoy/envoy.yaml`: + +- ✅ 添加 `serviceAccountName: envoy-gateway` 到 Deployment spec + +应用更新: + +```bash +kubectl apply -f deploy/k8s/envoy/envoy.yaml +``` + +验证部署: + +```bash +# 检查 ServiceAccount 已正确绑定 +kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName + +# 检查 Pod 状态 +kubectl get pod -n juwan -l app=envoy-gateway +``` + +### 第4步:启用 ETCD 加密(强烈推荐用于生产环境) + +这是一个集群级别的配置,需要在 Kubernetes 控制平面节点上执行。 + +**前提条件:** +- 具有 Kubernetes 集群管理员权限 +- 可以访问控制平面节点 +- 备份 ETCD 数据库 + +**步骤:** + +1. **生成加密密钥** + ```bash + head -c 32 /dev/urandom | base64 + ``` + 记录输出的 Base64 密钥。 + +2. **创建加密配置文件** + + 在控制平面节点上,创建 `/etc/kubernetes/encryption-config.yaml`: + + ```yaml + apiVersion: apiserver.config.k8s.io/v1 + kind: EncryptionConfiguration + resources: + - resources: + - secrets + providers: + - aescbc: + keys: + - name: key1 + secret: + - identity: {} + ``` + + 替换 `` 为第1步生成的密钥。 + +3. **修改 kube-apiserver 配置** + + 在控制平面节点上,编辑 `/etc/kubernetes/manifests/kube-apiserver.yaml`: + + **添加参数:** + ```yaml + spec: + containers: + - name: kube-apiserver + command: + - kube-apiserver + - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml + ``` + + **添加卷挂载:** + ```yaml + spec: + containers: + - name: kube-apiserver + volumeMounts: + - name: encryption-config + mountPath: /etc/kubernetes + readOnly: true + + volumes: + - name: encryption-config + hostPath: + path: /etc/kubernetes + type: DirectoryOrCreate + ``` + +4. **重启 kube-apiserver** + + 修改清单后,kubelet 会自动重启 kube-apiserver。检查状态: + + ```bash + # 在控制平面节点 + kubectl get pods -n kube-system | grep kube-apiserver + + # 监控重启过程 + kubectl logs -n kube-system -l component=kube-apiserver -f + ``` + +5. **验证加密是否启用** + + 创建一个新 Secret 并检查它在 ETCD 中是否加密: + + ```bash + # 创建测试 Secret + kubectl create secret generic test-secret -n juwan --from-literal=key=value + + # 从 control plane 节点检查 ETCD 数据 + # 如果数据不可读并包含加密标记,说明加密已启用 + ``` + +6. **保存加密密钥** + + ⚠️ **关键:将加密密钥安全地保存在离线存储中** + - 密钥丢失后,无法解密 ETCD 中的数据 + - 无法恢复任何 Secrets + - 建议用密码管理工具(如 HashiCorp Vault)或 HSM 存储密钥 + +### 第5步:验证整个系统 + +完整的验证清单: + +```bash +# 检查所有 Secrets 已创建 +kubectl get secret -n juwan +kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d + +# 检查 ServiceAccounts 已创建 +kubectl get sa -n juwan +kubectl describe sa user-rpc -n juwan +kubectl describe sa envoy-gateway -n juwan + +# 检查 RBAC 权限 +kubectl get role -n juwan +kubectl get rolebinding -n juwan +kubectl describe role jwt-secret-reader -n juwan + +# 测试权限:user-rpc 可以读 jwt-secret +kubectl auth can-i get secrets --as=system:serviceaccount:juwan:user-rpc --resource-name=jwt-secret -n juwan + +# 测试权限:envoy-gateway 可以读 jwt-secret +kubectl auth can-i get secrets --as=system:serviceaccount:juwan:envoy-gateway --resource-name=jwt-secret -n juwan + +# 测试权限:其他 ServiceAccount 无法读取 +kubectl auth can-i get secrets --as=system:serviceaccount:juwan:other-service -n juwan + +# 检查 Deployments 已正确配置 +kubectl get deployment user-rpc -n juwan -o yaml | grep -A 2 serviceAccountName +kubectl get deployment envoy-gateway -n juwan -o yaml | grep -A 2 serviceAccountName + +# 检查 Pods 是否已启动并运行 +kubectl get pods -n juwan -l app=user-rpc +kubectl get pods -n juwan -l app=envoy-gateway + +# 查看 JWT Secret 是否已挂载到 Pod +kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep JWT_SECRET_KEY +``` + +## 监控和日志 + +### 查看 Pod 日志 + +```bash +# user-rpc 日志 +kubectl logs -n juwan -l app=user-rpc -f --all-containers=true + +# Envoy 日志 +kubectl logs -n juwan -l app=envoy-gateway -f +``` + +### 检查 Pod 事件 + +```bash +# 查看 Pod 创建和启动事件 +kubectl describe pod -n juwan -l app=user-rpc +kubectl describe pod -n juwan -l app=envoy-gateway +``` + +### 权限问题排查 + +如果 Pod 无法读取 Secret: + +```bash +# 检查 Pod 使用的 ServiceAccount +kubectl get pod POD_NAME -n juwan -o yaml | grep serviceAccountName + +# 检查 RBAC 绑定 +kubectl get rolebinding -n juwan -o wide + +# 检查 Role 权限定义 +kubectl get role jwt-secret-reader -n juwan -o yaml + +# 尝试用 Pod 的身份读取 Secret(需要 kubectl-user-impersonate 或类似工具) +kubectl get secret jwt-secret --as=system:serviceaccount:juwan:user-rpc -n juwan +``` + +## 安全最佳实践 + +### 1. 密钥轮换 + +周期性更换 JWT 秘钥(建议每季度): + +```bash +# 1. 生成新密钥 +NEW_KEY=$(head -c 32 /dev/urandom | base64) + +# 2. 更新 Secret +kubectl create secret generic jwt-secret \ + --from-literal=secret-key=$NEW_KEY \ + --dry-run=client -o yaml | kubectl apply -f - + +# 3. 重启 Pods 以加载新密钥(滚动更新) +kubectl rollout restart deployment/user-rpc -n juwan +kubectl rollout restart deployment/envoy-gateway -n juwan + +# 4. 验证新 Pods 已启动并运行 +kubectl rollout status deployment/user-rpc -n juwan +kubectl rollout status deployment/envoy-gateway -n juwan + +# 5. 已颁发的旧令牌将变为无效 +# 需要用户重新登录获取新令牌 +``` + +### 2. 审计和监控 + +在生产环境中启用 Kubernetes 审计日志来跟踪 Secret 访问: + +```yaml +# kube-apiserver 审计策略示例 +apiVersion: audit.k8s.io/v1 +kind: Policy +rules: + # 记录 secret 资源的访问 + - level: RequestResponse + verbs: ["get", "list", "watch"] + resources: ["secrets"] + # 记录所有认证失败 + - level: RequestResponse + omitStages: + - RequestReceived + userGroups: ["system:unauthenticated"] + - level: Metadata + omitStages: + - RequestReceived +``` + +### 3. 网络策略 + +使用 NetworkPolicy 限制 Pods 之间的通信: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: jwt-secret-access + namespace: juwan +spec: + podSelector: + matchLabels: + app: jwt-secret-reader + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app: user-rpc + - podSelector: + matchLabels: + app: envoy-gateway + ports: + - protocol: TCP + port: 443 # API Server +``` + +## 灾难恢复 + +### 备份 Secret 和 RBAC 配置 + +```bash +# 备份 JWT Secret +kubectl get secret jwt-secret -n juwan -o yaml > jwt-secret-backup.yaml + +# 备份 RBAC 配置 +kubectl get role jwt-secret-reader -n juwan -o yaml > jwt-role-backup.yaml +kubectl get rolebinding -n juwan -l app=jwt-secret-reader -o yaml > jwt-rolebinding-backup.yaml + +# 加密备份文件 +gpg --symmetric jwt-secret-backup.yaml +``` + +### 恢复步骤 + +如果 Secret 被意外删除: + +```bash +# 从备份恢复 +kubectl apply -f jwt-secret-backup.yaml + +# 重启 Pods 以重新加载 Secret +kubectl rollout restart deployment/user-rpc -n juwan +kubectl rollout restart deployment/envoy-gateway -n juwan +``` + +## 常见问题 + +### Q: Pod 无法启动,显示 "failed to pull secret" + +A: 检查: +1. Secret 是否存在:`kubectl get secret jwt-secret -n juwan` +2. ServiceAccount 是否绑定了 RBAC:`kubectl describe rolebinding -n juwan` +3. Secret 名称和命名空间是否正确 + +### Q: 加密后如何验证 ETCD 中的数据已加密? + +A: 从 control plane 节点: +```bash +# 直接读取 ETCD(如果配置了加密,数据应该不可读) +sudo strings /var/lib/etcd/member/snap/db | grep -i secret +``` + +### Q: 能否更改加密密钥而不重新创建 ETCD? + +A: 可以,但流程复杂: +1. 更新 encryption-config.yaml 中的新密钥 +2. 将新密钥添加到提供程序列表(保持旧密钥) +3. 重启 kube-apiserver +4. 触发重新加密:`kubectl get all --all-namespaces -o json | kubectl apply -f -` + +### Q: 如何在 Minikube 中启用 ETCD 加密? + +A: 参考 `ENCRYPTION.md` 中的 Minikube 特定说明部分。 + +## 相关文件 + +- `jwt-secret.yaml` - Secret 和 RBAC 配置 +- `ENCRYPTION.md` - ETCD 加密详细文档 +- `README.md` - 快速参考指南 +- `/deploy/k8s/service/user/user-rpc.yaml` - user-rpc Deployment 配置 +- `/deploy/k8s/envoy/envoy.yaml` - Envoy 网关 Deployment 配置 + +## 下一步 + +部署完成后: + +1. **集成 JWT 验证到 RPC Handlers** + - 实现 gRPC unary interceptor + - 验证令牌有效性 + - 处理令牌刷新逻辑 + +2. **集成 JWT 验证到 Envoy** + - 扩展 Lua filter 进行令牌验证 + - 返回 401(无效令牌)或 200(有效令牌) + +3. **端到端测试** + - 创建用户和登录 + - 生成和验证 JWT + - 测试令牌刷新和撤销 + - 验证 ETCD 加密 + +4. **生产部署** + - 启用审计日志 + - 配置密钥轮换计划 + - 建立备份和恢复流程 + - 监控 Secret 访问 diff --git a/docs/secrets/ENCRYPTION.md b/docs/secrets/ENCRYPTION.md index 973ed36..73e8e85 100644 --- a/docs/secrets/ENCRYPTION.md +++ b/docs/secrets/ENCRYPTION.md @@ -1,129 +1,129 @@ -# ETCD Encryption Configuration for Kubernetes - -To enable static encryption at rest for Kubernetes secrets in ETCD, you need to configure the API Server with an EncryptionConfiguration. - -## 1. Generate an Encryption Key - -```bash -# Generate a 32-byte base64-encoded key -head -c 32 /dev/urandom | base64 -# Example output: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4= -``` - -## 2. Create EncryptionConfiguration File - -Create `/etc/kubernetes/encryption-config.yaml` on the control plane node: - -```yaml -apiVersion: apiserver.config.k8s.io/v1 -kind: EncryptionConfiguration -resources: - - resources: - - secrets - providers: - - aescbc: - keys: - - name: key1 - secret: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4= - - identity: {} -``` - -## 3. Update kube-apiserver Static Pod Manifest - -Edit `/etc/kubernetes/manifests/kube-apiserver.yaml` on the control plane node: - -```yaml -spec: - containers: - - name: kube-apiserver - command: - - kube-apiserver - # ... existing flags ... - - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml - volumeMounts: - - name: encryption-config - mountPath: /etc/kubernetes - readOnly: true - volumes: - - name: encryption-config - hostPath: - path: /etc/kubernetes - type: DirectoryOrCreate -``` - -## 4. Verify Encryption is Working - -```bash -# After restarting the API server, create a secret and verify it's encrypted -kubectl create secret generic test-secret --from-literal=key=value -n juwan - -# Check if the secret is encrypted in etcd -kubectl get secret test-secret -o yaml - -# You can also check raw etcd data (requires etcd access): -# etcdctl --endpoints=https://127.0.0.1:2379 get /kubernetes.io/secrets/juwan/test-secret -# The data should be encrypted (not human-readable) -``` - -## 5. Important Notes - -- **Backup your encryption key** in a secure location -- **Never commit encryption keys** to version control -- If you lose the key, all encrypted secrets will be unrecoverable -- After enabling encryption, existing unencrypted secrets will not be automatically encrypted - - To encrypt existing secrets, you can use: `kubectl delete secret && kubectl create secret ...` - - Or use: `kubectl patch secret -p '{}' --type=merge` (triggers re-encryption) - -## 6. RBAC Configuration for JWT Secret - -The `jwt-secret.yaml` includes RBAC rules that: -- Create a `jwt-secret` Secret in the `juwan` namespace -- Create ServiceAccounts for `user-rpc` and `envoy-gateway` -- Create a Role `jwt-secret-reader` that allows reading only the `jwt-secret` Secret -- Bind this Role to both ServiceAccounts via RoleBindings - -This ensures: -- Only `user-rpc` and `envoy-gateway` Pods can read the JWT secret -- Other services and users cannot access the JWT secret -- Least privilege access principle is enforced - -## 7. Update Deployment to Use ServiceAccount - -Make sure your Deployment references the ServiceAccount: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: user-rpc - namespace: juwan -spec: - template: - spec: - serviceAccountName: user-rpc # This is important! - containers: - - name: user-rpc - env: - - name: JWT_SECRET_KEY - valueFrom: - secretKeyRef: - name: jwt-secret - key: secret-key -``` - -## 8. For Minikube Users - -If using Minikube, you can enable encryption with: - -```bash -minikube config set apiserver.encryption-provider-config /path/to/encryption-config.yaml -minikube start -``` - -Or manually edit the kube-apiserver manifest after starting Minikube: - -```bash -minikube ssh -sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml -# Add the flags and volume mounts as shown above -``` +# ETCD Encryption Configuration for Kubernetes + +To enable static encryption at rest for Kubernetes secrets in ETCD, you need to configure the API Server with an EncryptionConfiguration. + +## 1. Generate an Encryption Key + +```bash +# Generate a 32-byte base64-encoded key +head -c 32 /dev/urandom | base64 +# Example output: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4= +``` + +## 2. Create EncryptionConfiguration File + +Create `/etc/kubernetes/encryption-config.yaml` on the control plane node: + +```yaml +apiVersion: apiserver.config.k8s.io/v1 +kind: EncryptionConfiguration +resources: + - resources: + - secrets + providers: + - aescbc: + keys: + - name: key1 + secret: sxFdbKYquCe3EbRWVV+pFe2lS8K8hbiv3V8ExQZ0fD4= + - identity: {} +``` + +## 3. Update kube-apiserver Static Pod Manifest + +Edit `/etc/kubernetes/manifests/kube-apiserver.yaml` on the control plane node: + +```yaml +spec: + containers: + - name: kube-apiserver + command: + - kube-apiserver + # ... existing flags ... + - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml + volumeMounts: + - name: encryption-config + mountPath: /etc/kubernetes + readOnly: true + volumes: + - name: encryption-config + hostPath: + path: /etc/kubernetes + type: DirectoryOrCreate +``` + +## 4. Verify Encryption is Working + +```bash +# After restarting the API server, create a secret and verify it's encrypted +kubectl create secret generic test-secret --from-literal=key=value -n juwan + +# Check if the secret is encrypted in etcd +kubectl get secret test-secret -o yaml + +# You can also check raw etcd data (requires etcd access): +# etcdctl --endpoints=https://127.0.0.1:2379 get /kubernetes.io/secrets/juwan/test-secret +# The data should be encrypted (not human-readable) +``` + +## 5. Important Notes + +- **Backup your encryption key** in a secure location +- **Never commit encryption keys** to version control +- If you lose the key, all encrypted secrets will be unrecoverable +- After enabling encryption, existing unencrypted secrets will not be automatically encrypted + - To encrypt existing secrets, you can use: `kubectl delete secret && kubectl create secret ...` + - Or use: `kubectl patch secret -p '{}' --type=merge` (triggers re-encryption) + +## 6. RBAC Configuration for JWT Secret + +The `jwt-secret.yaml` includes RBAC rules that: +- Create a `jwt-secret` Secret in the `juwan` namespace +- Create ServiceAccounts for `user-rpc` and `envoy-gateway` +- Create a Role `jwt-secret-reader` that allows reading only the `jwt-secret` Secret +- Bind this Role to both ServiceAccounts via RoleBindings + +This ensures: +- Only `user-rpc` and `envoy-gateway` Pods can read the JWT secret +- Other services and users cannot access the JWT secret +- Least privilege access principle is enforced + +## 7. Update Deployment to Use ServiceAccount + +Make sure your Deployment references the ServiceAccount: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-rpc + namespace: juwan +spec: + template: + spec: + serviceAccountName: user-rpc # This is important! + containers: + - name: user-rpc + env: + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: jwt-secret + key: secret-key +``` + +## 8. For Minikube Users + +If using Minikube, you can enable encryption with: + +```bash +minikube config set apiserver.encryption-provider-config /path/to/encryption-config.yaml +minikube start +``` + +Or manually edit the kube-apiserver manifest after starting Minikube: + +```bash +minikube ssh +sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml +# Add the flags and volume mounts as shown above +``` diff --git a/docs/secrets/FLOWCHART.md b/docs/secrets/FLOWCHART.md index 4cf5bcc..7446ac8 100644 --- a/docs/secrets/FLOWCHART.md +++ b/docs/secrets/FLOWCHART.md @@ -1,415 +1,415 @@ -# 部署流程图和时间线 - -## 部署架构流程图 - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ JWT 认证系统部署流程 │ -└──────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────────┐ -│ Phase 1: 前置检查 (5分钟) │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ ✓ Kubernetes 集群版本 >= 1.24 │ -│ ✓ kubectl 已配置,可访问集群 │ -│ ✓ juwan namespace 已存在 │ -│ ✓ redis-operator CRD 已安装 │ -│ ✓ 集群管理员权限(用于 ETCD 加密) │ -│ │ -│ 命令检查: │ -│ $ kubectl cluster-info │ -│ $ kubectl get ns juwan │ -│ $ kubectl get crd redisclusters.redis.redis.opstreelabs.in │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Phase 2: 创建 Secret 和 RBAC (5分钟) ⚡ 必需 │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ 执行: │ -│ $ kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml │ -│ │ -│ 创建的资源: │ -│ ✓ Secret: jwt-secret (包含 JWT 秘钥) │ -│ ✓ ServiceAccount: user-rpc │ -│ ✓ ServiceAccount: envoy-gateway │ -│ ✓ Role: jwt-secret-reader (只读权限) │ -│ ✓ RoleBinding: jwt-secret-reader-user-rpc │ -│ ✓ RoleBinding: jwt-secret-reader-envoy-gateway │ -│ │ -│ 验证: │ -│ $ kubectl get secret jwt-secret -n juwan │ -│ $ kubectl get sa -n juwan | grep -E "user-rpc|envoy" │ -│ $ kubectl get role -n juwan │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Phase 3: 更新 Deployments (10分钟) ⚡ 必需 │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ Step 3a: 更新 user-rpc Deployment │ -│ 执行: │ -│ $ kubectl apply -f deploy/k8s/service/user/user-rpc.yaml │ -│ │ -│ 变更: │ -│ - serviceAccountName: user-rpc (绑定权限) │ -│ - env.JWT_SECRET_KEY (从 Secret 挂载) │ -│ - 保持 Redis Cluster 配置 │ -│ │ -│ 等待 Pods 启动: │ -│ $ kubectl rollout status deployment/user-rpc -n juwan │ -│ │ -│ --- │ -│ │ -│ Step 3b: 更新 Envoy Gateway Deployment │ -│ 执行: │ -│ $ kubectl apply -f deploy/k8s/envoy/envoy.yaml │ -│ │ -│ 变更: │ -│ - serviceAccountName: envoy-gateway (绑定权限) │ -│ - 保持 CSRF Lua 防护配置 │ -│ │ -│ 等待 Pods 启动: │ -│ $ kubectl rollout status deployment/envoy-gateway -n juwan │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────┐ -│ Phase 4: 验证部署 (15分钟) ⚡ 必需 │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ 检查清单: │ -│ │ -│ 1️⃣ Secret 和权限验证 │ -│ $ kubectl get secret jwt-secret -n juwan │ -│ $ kubectl get role jwt-secret-reader -n juwan │ -│ $ kubectl get rolebinding -n juwan | grep jwt-secret │ -│ │ -│ 2️⃣ 权限测试 │ -│ $ kubectl auth can-i get secrets \ │ -│ --as=system:serviceaccount:juwan:user-rpc \ │ -│ --resource-name=jwt-secret -n juwan │ -│ 预期: yes │ -│ │ -│ 3️⃣ Pods 运行状态 │ -│ $ kubectl get pods -n juwan -l app=user-rpc │ -│ $ kubectl get pods -n juwan -l app=envoy-gateway │ -│ 预期: 3 个 user-rpc Pods + 1 个 envoy-gateway Pod 都在 Running │ -│ │ -│ 4️⃣ 环境变量验证 │ -│ $ kubectl exec -it -n juwan -- env | grep JWT │ -│ 预期: JWT_SECRET_KEY=... │ -│ │ -│ 5️⃣ Redis 连接验证 │ -│ $ kubectl run redis-cli --image=redis:latest --rm -it \ │ -│ -- redis-cli -h user-redis.juwan:6379 PING │ -│ 预期: PONG │ -│ │ -│ 详见: VERIFICATION.md 第1-8部分 │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ - │ - ├─────────── 生产环境额外步骤 ──────────────┐ - │ │ - ▼ ▼ -┌──────────────────────────┐ ┌─────────────────────────────────────┐ -│ Phase 5a: 应用集成 │ │ Phase 5b: 启用 ETCD 加密 (30分钟) │ -│ (2-3 小时) ⚠️ 推荐 │ │ ⚠️ 生产推荐,需集群管理员权限 │ -├──────────────────────────┤ ├─────────────────────────────────────┤ -│ │ │ │ -│ 实施内容: │ │ 前提条件: │ -│ ✓ gRPC Interceptor │ │ ✓ Control Plane 节点访问权限 │ -│ ✓ Login/Logout Handler │ │ ✓ ETCD 备份已创建 │ -│ ✓ JWT Middleware │ │ ✓ 加密密钥已生成 │ -│ ✓ Token Refresh Logic │ │ │ -│ ✓ Error Handling │ │ 步骤: │ -│ ✓ Unit Tests │ │ 1. 生成 32 字节密钥 │ -│ │ │ $ head -c 32 /dev/urandom | base64 -│ 参考: │ │ │ -│ INTEGRATION.md │ │ 2. 创建加密配置文件 │ -│ │ │ /etc/kubernetes/encryption-config.yaml -│ 时间估计: │ │ │ -│ - gRPC 拦截器: 30分钟 │ │ 3. 修改 kube-apiserver manifest │ -│ - Handlers: 60分钟 │ │ 添加密钥路径和卷挂载 │ -│ - Middleware: 30分钟 │ │ │ -│ - 测试: 60分钟 │ │ 4. 重启 kube-apiserver │ -│ │ │ kubelet 自动重启 │ -│ │ │ │ -│ │ │ 5. 验证加密已启用 │ -│ │ │ kubectl create secret generic ... │ -│ │ │ 检查 ETCD 中的数据是否加密 │ -│ │ │ │ -│ │ │ 详见: ENCRYPTION.md (8个部分) │ -│ │ │ 验证: VERIFICATION.md 第9部分 │ -│ │ │ │ -└──────────────────────────┘ └─────────────────────────────────────┘ - │ │ - └───────────────────┬─────────────────────┘ - │ - ▼ - ┌──────────────────────────────────────────┐ - │ Phase 6: 完成 ✅ │ - ├──────────────────────────────────────────┤ - │ │ - │ 最终检查: │ - │ ✓ 所有 Pods 运行正常 │ - │ ✓ RBAC 权限已验证 │ - │ ✓ JWT 功能已集成 │ - │ ✓ 日志和监控已配置 │ - │ ✓ (可选) ETCD 加密已启用 │ - │ │ - │ 生产推荐: │ - │ ✓ 启用审计日志 │ - │ ✓ 配置密钥轮换计划(季度) │ - │ ✓ 备份密钥到安全位置 │ - │ ✓ 配置告警和监控 │ - │ │ - └──────────────────────────────────────────┘ -``` - -## 时间估计和路径 - -``` -推荐部署路径 -═════════════════════════════════════════════════════════════════ - -🚀 最快路径 (30 分钟) - 开发环境 -──────────────────────────────────────────────────────────────── -Phase 1: 前置检查 ⏱️ 5 分钟 -Phase 2: 创建 Secret 和 RBAC ⏱️ 5 分钟 -Phase 3: 更新 Deployments ⏱️ 10 分钟 -Phase 4: 验证部署 ⏱️ 10 分钟 -──────────────────────────────────────────────────────────────── - 总计: 30 分钟 - - -📊 默认路径 (75 分钟) - 测试环境 -──────────────────────────────────────────────────────────────── -Phase 1-4: 如上 ⏱️ 30 分钟 -Phase 5a: 应用集成(简单版) ⏱️ 45 分钟 -──────────────────────────────────────────────────────────────── - 总计: 75 分钟 - - -🏆 完整路径 (3.5-4 小时) - 生产环境 -──────────────────────────────────────────────────────────────── -Phase 1-4: 如上 ⏱️ 30 分钟 -Phase 5a: 应用集成(完整版) ⏱️ 2-3 小时 -Phase 5b: ETCD 加密配置 ⏱️ 30 分钟 -──────────────────────────────────────────────────────────────── - 总计: 3.5-4 小时 - - -📚 学习路径 (6-8 小时) - 从零开始理解 -──────────────────────────────────────────────────────────────── -文档阅读 (SUMMARY + DEPLOYMENT + INTEGRATION) ⏱️ 1-2 小时 -完整路径部署 ⏱️ 3.5-4 小时 -验证和测试 ⏱️ 1-2 小时 -──────────────────────────────────────────────────────────────── - 总计: 6-8 小时 -``` - -## 并行和串行步骤 - -``` -可以并行执行的任务 -═════════════════════════════════════════════════════════════════ - -┌─────────────────────────────────┐ -│ 应用集成 (Phase 5a) │ ────┐ -├─────────────────────────────────┤ │ 可选,独立 -│ • gRPC interceptor │ │ 进行 -│ • REST middleware │ │ -│ • Handler 实现 │ │ -│ • 单元测试 │ │ -└─────────────────────────────────┘ │ - ├─ 与 Phase 2-4 并行 - │ -┌─────────────────────────────────┐ │ -│ ETCD 加密 (Phase 5b) │ ────┘ -├─────────────────────────────────┤ │ 需要集群管理员 -│ • 生成密钥(可单独进行) │ │ 权限,在 -│ • 创建配置文件 │ │ Control Plane -│ • 修改 kube-apiserver │ │ 节点执行 -│ • 重启 API server │ │ -└─────────────────────────────────┘────┘ - - -必须串行执行的步骤 -═════════════════════════════════════════════════════════════════ - -Phase 1 → Phase 2 → Phase 3 → Phase 4 → (Phase 5a + Phase 5b) - - ↓ ↓ ↓ ↓ -前置检查 创建资源 部署应用 验证完整 可选扩展功能 - -• Phase 2 必须在 Phase 1 之后(需要 namespace) -• Phase 3 必须在 Phase 2 之后(需要 RoleBinding) -• Phase 4 必须在 Phase 3 之后(需要 Pods 启动) -• Phase 5a/5b 可在 Phase 4 完成后并行进行 -``` - -## 关键时间点 - -``` -事件时间线 -═════════════════════════════════════════════════════════════════ - -T+0 Phase 1: 验证前置条件 - └─ 预计 5 分钟 - -T+5 Phase 2: kubectl apply jwt-secret.yaml - └─ 预计 1 分钟执行,5 分钟验证 - -T+11 Phase 3a: kubectl apply user-rpc.yaml - └─ 3 个 Pods 启动(滚动更新) - └─ 预计 ~3 分钟(取决于镜像拉取) - -T+14 Phase 3b: kubectl apply envoy.yaml - └─ 1 个 Pod 启动 - └─ 预计 ~2 分钟 - -T+16 Phase 4: 执行完整验证检查 - └─ 12 个验证部分,共 ~15 分钟 - -T+31 ✅ 基础部署完成 - -T+31 (可选) Phase 5a: 应用代码集成 -到 └─ 2-3 小时编码和测试 -T+211 - -T+31 (可选) Phase 5b: ETCD 加密 -到 └─ 30 分钟配置 -T+61 - -T+211 或 T+61 🎉 全部完成 -(取决于是否执行 Phase 5) -``` - -## 推荐的部署顺序 - -### 对于 DevOps/SRE - -``` -优先级顺序: - -1️⃣ Phase 1-4 (核心部署) [必需] - └─ 时间: 30 分钟 - -2️⃣ Phase 5b (ETCD 加密) [生产强烈推荐] - └─ 时间: 30 分钟 - └─ 开始时间: T+16 之前 (与 Phase 4 并行) - -3️⃣ 密钥备份和恢复计划 [重要] - └─ 时间: 15 分钟 - └─ 参考: DEPLOYMENT.md 灾难恢复 - -4️⃣ Phase 5a 支持 [当开发完成时] - └─ 协助开发团队集成 JWT - └─ 参考: INTEGRATION.md -``` - -### 对于应用开发者 - -``` -优先级顺序: - -1️⃣ 了解系统架构 [了解背景] - └─ 文档: SUMMARY.md - └─ 时间: 10 分钟 - -2️⃣ Phase 5a: 代码集成 [并行进行] - └─ 参考: INTEGRATION.md - └─ 时间: 2-3 小时 - -3️⃣ 单元测试 [在开发中] - └─ 参考: INTEGRATION.md 第 9 部分 - └─ 时间: 1 小时 - -4️⃣ 集成测试 [与运维协调] - └─ 测试完整流程 - └─ 时间: 1-2 小时 -``` - -## 回滚应急步骤 - -如果部署失败,可以快速回滚: - -``` -紧急回滚 -═════════════════════════════════════════════════════════════════ - -如果 Phase 2 (Secret) 失败: -→ kubectl delete secret jwt-secret -n juwan -→ kubectl delete sa user-rpc envoy-gateway -n juwan -→ kubectl delete role jwt-secret-reader -n juwan -→ 修正配置后重新应用 - -如果 Phase 3 (Deployment) 失败: -→ kubectl rollout undo deployment/user-rpc -n juwan -→ kubectl rollout undo deployment/envoy-gateway -n juwan -→ 或删除部署并使用稳定的旧版本重新部署 - -如果 Phase 5b (ETCD 加密) 失败: -→ 从 kube-apiserver 清单中移除加密参数 -→ 重启 kube-apiserver -→ 删除 /etc/kubernetes/encryption-config.yaml -→ 从最近的 ETCD 备份恢复(如果需要) - -注意: 备份是关键!每次重大操作前都应备份。 -``` - -## 部署检查点 (Go/No-Go) - -``` -关键检查点 -═════════════════════════════════════════════════════════════════ - -✅ Checkpoint 1 (Phase 2 后) - - Secret 已创建 - - ServiceAccounts 已创建 - - Go → 继续 Phase 3 - - No-Go → 检查 kubectl 权限 - -✅ Checkpoint 2 (Phase 3 后) - - Pods 已启动 (Running) - - No PodSchedulingFailure - - Go → 继续 Phase 4 - - No-Go → 检查资源限制和镜像拉取 - -✅ Checkpoint 3 (Phase 4 权限测试) - - user-rpc 可以读 jwt-secret - - envoy-gateway 可以读 jwt-secret - - 其他 SA 无法读取 - - Go → 继续 Phase 5 - - No-Go → 检查 RBAC 配置 - -✅ Checkpoint 4 (Phase 4 Redis 连接) - - Redis Cluster 健康 (3/3 nodes) - - PING 返回 PONG - - Go → 继续应用集成 - - No-Go → 检查 Redis Pods 和网络 - -✅ Checkpoint 5 (Phase 5b - ETCD 加密) - - 新创建的 Secret 在 ETCD 中已加密 - - Go → 生产就绪 - - No-Go → 检查 kube-apiserver 配置参数 -``` - ---- - -## 使用这个流程图 - -1. **首次部署** → 从 "推荐部署路径" 选择合适的版本 -2. **卡在某一步** → 查看对应的 Phase 描述和命令 -3. **估算时间** → 查看 "时间估计和路径" 部分 -4. **需要回滚** → 参考 "回滚应急步骤" -5. **检查进度** → 使用 "部署检查点" - -详细的部署步骤见:[DEPLOYMENT.md](./DEPLOYMENT.md) +# 部署流程图和时间线 + +## 部署架构流程图 + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ JWT 认证系统部署流程 │ +└──────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────┐ +│ Phase 1: 前置检查 (5分钟) │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ✓ Kubernetes 集群版本 >= 1.24 │ +│ ✓ kubectl 已配置,可访问集群 │ +│ ✓ juwan namespace 已存在 │ +│ ✓ redis-operator CRD 已安装 │ +│ ✓ 集群管理员权限(用于 ETCD 加密) │ +│ │ +│ 命令检查: │ +│ $ kubectl cluster-info │ +│ $ kubectl get ns juwan │ +│ $ kubectl get crd redisclusters.redis.redis.opstreelabs.in │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Phase 2: 创建 Secret 和 RBAC (5分钟) ⚡ 必需 │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ 执行: │ +│ $ kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml │ +│ │ +│ 创建的资源: │ +│ ✓ Secret: jwt-secret (包含 JWT 秘钥) │ +│ ✓ ServiceAccount: user-rpc │ +│ ✓ ServiceAccount: envoy-gateway │ +│ ✓ Role: jwt-secret-reader (只读权限) │ +│ ✓ RoleBinding: jwt-secret-reader-user-rpc │ +│ ✓ RoleBinding: jwt-secret-reader-envoy-gateway │ +│ │ +│ 验证: │ +│ $ kubectl get secret jwt-secret -n juwan │ +│ $ kubectl get sa -n juwan | grep -E "user-rpc|envoy" │ +│ $ kubectl get role -n juwan │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Phase 3: 更新 Deployments (10分钟) ⚡ 必需 │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Step 3a: 更新 user-rpc Deployment │ +│ 执行: │ +│ $ kubectl apply -f deploy/k8s/service/user/user-rpc.yaml │ +│ │ +│ 变更: │ +│ - serviceAccountName: user-rpc (绑定权限) │ +│ - env.JWT_SECRET_KEY (从 Secret 挂载) │ +│ - 保持 Redis Cluster 配置 │ +│ │ +│ 等待 Pods 启动: │ +│ $ kubectl rollout status deployment/user-rpc -n juwan │ +│ │ +│ --- │ +│ │ +│ Step 3b: 更新 Envoy Gateway Deployment │ +│ 执行: │ +│ $ kubectl apply -f deploy/k8s/envoy/envoy.yaml │ +│ │ +│ 变更: │ +│ - serviceAccountName: envoy-gateway (绑定权限) │ +│ - 保持 CSRF Lua 防护配置 │ +│ │ +│ 等待 Pods 启动: │ +│ $ kubectl rollout status deployment/envoy-gateway -n juwan │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ Phase 4: 验证部署 (15分钟) ⚡ 必需 │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ 检查清单: │ +│ │ +│ 1️⃣ Secret 和权限验证 │ +│ $ kubectl get secret jwt-secret -n juwan │ +│ $ kubectl get role jwt-secret-reader -n juwan │ +│ $ kubectl get rolebinding -n juwan | grep jwt-secret │ +│ │ +│ 2️⃣ 权限测试 │ +│ $ kubectl auth can-i get secrets \ │ +│ --as=system:serviceaccount:juwan:user-rpc \ │ +│ --resource-name=jwt-secret -n juwan │ +│ 预期: yes │ +│ │ +│ 3️⃣ Pods 运行状态 │ +│ $ kubectl get pods -n juwan -l app=user-rpc │ +│ $ kubectl get pods -n juwan -l app=envoy-gateway │ +│ 预期: 3 个 user-rpc Pods + 1 个 envoy-gateway Pod 都在 Running │ +│ │ +│ 4️⃣ 环境变量验证 │ +│ $ kubectl exec -it -n juwan -- env | grep JWT │ +│ 预期: JWT_SECRET_KEY=... │ +│ │ +│ 5️⃣ Redis 连接验证 │ +│ $ kubectl run redis-cli --image=redis:latest --rm -it \ │ +│ -- redis-cli -h user-redis.juwan:6379 PING │ +│ 预期: PONG │ +│ │ +│ 详见: VERIFICATION.md 第1-8部分 │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ├─────────── 生产环境额外步骤 ──────────────┐ + │ │ + ▼ ▼ +┌──────────────────────────┐ ┌─────────────────────────────────────┐ +│ Phase 5a: 应用集成 │ │ Phase 5b: 启用 ETCD 加密 (30分钟) │ +│ (2-3 小时) ⚠️ 推荐 │ │ ⚠️ 生产推荐,需集群管理员权限 │ +├──────────────────────────┤ ├─────────────────────────────────────┤ +│ │ │ │ +│ 实施内容: │ │ 前提条件: │ +│ ✓ gRPC Interceptor │ │ ✓ Control Plane 节点访问权限 │ +│ ✓ Login/Logout Handler │ │ ✓ ETCD 备份已创建 │ +│ ✓ JWT Middleware │ │ ✓ 加密密钥已生成 │ +│ ✓ Token Refresh Logic │ │ │ +│ ✓ Error Handling │ │ 步骤: │ +│ ✓ Unit Tests │ │ 1. 生成 32 字节密钥 │ +│ │ │ $ head -c 32 /dev/urandom | base64 +│ 参考: │ │ │ +│ INTEGRATION.md │ │ 2. 创建加密配置文件 │ +│ │ │ /etc/kubernetes/encryption-config.yaml +│ 时间估计: │ │ │ +│ - gRPC 拦截器: 30分钟 │ │ 3. 修改 kube-apiserver manifest │ +│ - Handlers: 60分钟 │ │ 添加密钥路径和卷挂载 │ +│ - Middleware: 30分钟 │ │ │ +│ - 测试: 60分钟 │ │ 4. 重启 kube-apiserver │ +│ │ │ kubelet 自动重启 │ +│ │ │ │ +│ │ │ 5. 验证加密已启用 │ +│ │ │ kubectl create secret generic ... │ +│ │ │ 检查 ETCD 中的数据是否加密 │ +│ │ │ │ +│ │ │ 详见: ENCRYPTION.md (8个部分) │ +│ │ │ 验证: VERIFICATION.md 第9部分 │ +│ │ │ │ +└──────────────────────────┘ └─────────────────────────────────────┘ + │ │ + └───────────────────┬─────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────────┐ + │ Phase 6: 完成 ✅ │ + ├──────────────────────────────────────────┤ + │ │ + │ 最终检查: │ + │ ✓ 所有 Pods 运行正常 │ + │ ✓ RBAC 权限已验证 │ + │ ✓ JWT 功能已集成 │ + │ ✓ 日志和监控已配置 │ + │ ✓ (可选) ETCD 加密已启用 │ + │ │ + │ 生产推荐: │ + │ ✓ 启用审计日志 │ + │ ✓ 配置密钥轮换计划(季度) │ + │ ✓ 备份密钥到安全位置 │ + │ ✓ 配置告警和监控 │ + │ │ + └──────────────────────────────────────────┘ +``` + +## 时间估计和路径 + +``` +推荐部署路径 +═════════════════════════════════════════════════════════════════ + +🚀 最快路径 (30 分钟) - 开发环境 +──────────────────────────────────────────────────────────────── +Phase 1: 前置检查 ⏱️ 5 分钟 +Phase 2: 创建 Secret 和 RBAC ⏱️ 5 分钟 +Phase 3: 更新 Deployments ⏱️ 10 分钟 +Phase 4: 验证部署 ⏱️ 10 分钟 +──────────────────────────────────────────────────────────────── + 总计: 30 分钟 + + +📊 默认路径 (75 分钟) - 测试环境 +──────────────────────────────────────────────────────────────── +Phase 1-4: 如上 ⏱️ 30 分钟 +Phase 5a: 应用集成(简单版) ⏱️ 45 分钟 +──────────────────────────────────────────────────────────────── + 总计: 75 分钟 + + +🏆 完整路径 (3.5-4 小时) - 生产环境 +──────────────────────────────────────────────────────────────── +Phase 1-4: 如上 ⏱️ 30 分钟 +Phase 5a: 应用集成(完整版) ⏱️ 2-3 小时 +Phase 5b: ETCD 加密配置 ⏱️ 30 分钟 +──────────────────────────────────────────────────────────────── + 总计: 3.5-4 小时 + + +📚 学习路径 (6-8 小时) - 从零开始理解 +──────────────────────────────────────────────────────────────── +文档阅读 (SUMMARY + DEPLOYMENT + INTEGRATION) ⏱️ 1-2 小时 +完整路径部署 ⏱️ 3.5-4 小时 +验证和测试 ⏱️ 1-2 小时 +──────────────────────────────────────────────────────────────── + 总计: 6-8 小时 +``` + +## 并行和串行步骤 + +``` +可以并行执行的任务 +═════════════════════════════════════════════════════════════════ + +┌─────────────────────────────────┐ +│ 应用集成 (Phase 5a) │ ────┐ +├─────────────────────────────────┤ │ 可选,独立 +│ • gRPC interceptor │ │ 进行 +│ • REST middleware │ │ +│ • Handler 实现 │ │ +│ • 单元测试 │ │ +└─────────────────────────────────┘ │ + ├─ 与 Phase 2-4 并行 + │ +┌─────────────────────────────────┐ │ +│ ETCD 加密 (Phase 5b) │ ────┘ +├─────────────────────────────────┤ │ 需要集群管理员 +│ • 生成密钥(可单独进行) │ │ 权限,在 +│ • 创建配置文件 │ │ Control Plane +│ • 修改 kube-apiserver │ │ 节点执行 +│ • 重启 API server │ │ +└─────────────────────────────────┘────┘ + + +必须串行执行的步骤 +═════════════════════════════════════════════════════════════════ + +Phase 1 → Phase 2 → Phase 3 → Phase 4 → (Phase 5a + Phase 5b) + + ↓ ↓ ↓ ↓ +前置检查 创建资源 部署应用 验证完整 可选扩展功能 + +• Phase 2 必须在 Phase 1 之后(需要 namespace) +• Phase 3 必须在 Phase 2 之后(需要 RoleBinding) +• Phase 4 必须在 Phase 3 之后(需要 Pods 启动) +• Phase 5a/5b 可在 Phase 4 完成后并行进行 +``` + +## 关键时间点 + +``` +事件时间线 +═════════════════════════════════════════════════════════════════ + +T+0 Phase 1: 验证前置条件 + └─ 预计 5 分钟 + +T+5 Phase 2: kubectl apply jwt-secret.yaml + └─ 预计 1 分钟执行,5 分钟验证 + +T+11 Phase 3a: kubectl apply user-rpc.yaml + └─ 3 个 Pods 启动(滚动更新) + └─ 预计 ~3 分钟(取决于镜像拉取) + +T+14 Phase 3b: kubectl apply envoy.yaml + └─ 1 个 Pod 启动 + └─ 预计 ~2 分钟 + +T+16 Phase 4: 执行完整验证检查 + └─ 12 个验证部分,共 ~15 分钟 + +T+31 ✅ 基础部署完成 + +T+31 (可选) Phase 5a: 应用代码集成 +到 └─ 2-3 小时编码和测试 +T+211 + +T+31 (可选) Phase 5b: ETCD 加密 +到 └─ 30 分钟配置 +T+61 + +T+211 或 T+61 🎉 全部完成 +(取决于是否执行 Phase 5) +``` + +## 推荐的部署顺序 + +### 对于 DevOps/SRE + +``` +优先级顺序: + +1️⃣ Phase 1-4 (核心部署) [必需] + └─ 时间: 30 分钟 + +2️⃣ Phase 5b (ETCD 加密) [生产强烈推荐] + └─ 时间: 30 分钟 + └─ 开始时间: T+16 之前 (与 Phase 4 并行) + +3️⃣ 密钥备份和恢复计划 [重要] + └─ 时间: 15 分钟 + └─ 参考: DEPLOYMENT.md 灾难恢复 + +4️⃣ Phase 5a 支持 [当开发完成时] + └─ 协助开发团队集成 JWT + └─ 参考: INTEGRATION.md +``` + +### 对于应用开发者 + +``` +优先级顺序: + +1️⃣ 了解系统架构 [了解背景] + └─ 文档: SUMMARY.md + └─ 时间: 10 分钟 + +2️⃣ Phase 5a: 代码集成 [并行进行] + └─ 参考: INTEGRATION.md + └─ 时间: 2-3 小时 + +3️⃣ 单元测试 [在开发中] + └─ 参考: INTEGRATION.md 第 9 部分 + └─ 时间: 1 小时 + +4️⃣ 集成测试 [与运维协调] + └─ 测试完整流程 + └─ 时间: 1-2 小时 +``` + +## 回滚应急步骤 + +如果部署失败,可以快速回滚: + +``` +紧急回滚 +═════════════════════════════════════════════════════════════════ + +如果 Phase 2 (Secret) 失败: +→ kubectl delete secret jwt-secret -n juwan +→ kubectl delete sa user-rpc envoy-gateway -n juwan +→ kubectl delete role jwt-secret-reader -n juwan +→ 修正配置后重新应用 + +如果 Phase 3 (Deployment) 失败: +→ kubectl rollout undo deployment/user-rpc -n juwan +→ kubectl rollout undo deployment/envoy-gateway -n juwan +→ 或删除部署并使用稳定的旧版本重新部署 + +如果 Phase 5b (ETCD 加密) 失败: +→ 从 kube-apiserver 清单中移除加密参数 +→ 重启 kube-apiserver +→ 删除 /etc/kubernetes/encryption-config.yaml +→ 从最近的 ETCD 备份恢复(如果需要) + +注意: 备份是关键!每次重大操作前都应备份。 +``` + +## 部署检查点 (Go/No-Go) + +``` +关键检查点 +═════════════════════════════════════════════════════════════════ + +✅ Checkpoint 1 (Phase 2 后) + - Secret 已创建 + - ServiceAccounts 已创建 + - Go → 继续 Phase 3 + - No-Go → 检查 kubectl 权限 + +✅ Checkpoint 2 (Phase 3 后) + - Pods 已启动 (Running) + - No PodSchedulingFailure + - Go → 继续 Phase 4 + - No-Go → 检查资源限制和镜像拉取 + +✅ Checkpoint 3 (Phase 4 权限测试) + - user-rpc 可以读 jwt-secret + - envoy-gateway 可以读 jwt-secret + - 其他 SA 无法读取 + - Go → 继续 Phase 5 + - No-Go → 检查 RBAC 配置 + +✅ Checkpoint 4 (Phase 4 Redis 连接) + - Redis Cluster 健康 (3/3 nodes) + - PING 返回 PONG + - Go → 继续应用集成 + - No-Go → 检查 Redis Pods 和网络 + +✅ Checkpoint 5 (Phase 5b - ETCD 加密) + - 新创建的 Secret 在 ETCD 中已加密 + - Go → 生产就绪 + - No-Go → 检查 kube-apiserver 配置参数 +``` + +--- + +## 使用这个流程图 + +1. **首次部署** → 从 "推荐部署路径" 选择合适的版本 +2. **卡在某一步** → 查看对应的 Phase 描述和命令 +3. **估算时间** → 查看 "时间估计和路径" 部分 +4. **需要回滚** → 参考 "回滚应急步骤" +5. **检查进度** → 使用 "部署检查点" + +详细的部署步骤见:[DEPLOYMENT.md](./DEPLOYMENT.md) diff --git a/docs/secrets/INDEX.md b/docs/secrets/INDEX.md index d4d165d..2df4d05 100644 --- a/docs/secrets/INDEX.md +++ b/docs/secrets/INDEX.md @@ -1,399 +1,399 @@ -# JWT + ETCD 加密系统文档索引 - -## 📚 文档完整导航 - -### 快速入门 (5-15分钟) - -**推荐路径:** 从上到下顺序阅读 - -1. **[SUMMARY.md](./SUMMARY.md)** ⭐ 从这里开始 - - 📋 项目概览和架构图 - - 🎯 核心特性一览 - - ✅ 生产就绪检查清单 - - 🚀 下一步行动计划 - -2. **[README.md](./README.md)** - - 🏃 4个快速部署步骤 - - 🔐 安全考虑事项 - - 🔄 密钥轮换程序 - - 🆘 故障排查 - -3. **[QUICK_REFERENCE.md](./QUICK_REFERENCE.md)** - - ⚡ 一页速查表 - - 📝 常见命令复制粘贴 - - 🗺️ 文档地图 - - 🎓 关键参数速知 - -### 部署实施 (30-60分钟) - -4. **[DEPLOYMENT.md](./DEPLOYMENT.md)** - 最详细的部署指南 - - 📦 第1步:创建 Secret 和 RBAC(必需) - - 🔄 第2步:更新 user-rpc Deployment - - 🌐 第3步:更新 Envoy Gateway Deployment - - 🔐 第4步:启用 ETCD 加密(生产推荐) - - ✔️ 第5步:验证整个系统 - - 📊 监控和日志配置 - - 🛠️ 安全最佳实践 - - 🆘 故障排查指南 - - 💾 灾难恢复流程 - -### 验证和监控 (20-30分钟) - -5. **[VERIFICATION.md](./VERIFICATION.md)** - 完整验证清单 - - **12个验证部分:** - - | 部分 | 用途 | 时间 | - |-----|------|------| - | 第1部分 | Secret/RBAC 基础验证 | 2分钟 | - | 第2部分 | 权限测试(allow/deny) | 3分钟 | - | 第3部分 | Deployment 配置检查 | 2分钟 | - | 第4部分 | Redis 连接测试 | 2分钟 | - | 第5部分 | 应用启动日志 | 3分钟 | - | 第6部分 | 网络和服务发现 | 3分钟 | - | 第7部分 | Prometheus 指标 | 3分钟 | - | 第8部分 | Loki 日志聚合 | 2分钟 | - | 第9部分 | ETCD 加密验证 | 5分钟 | - | 第10部分 | JWT 功能测试 | 10分钟 | - | 第11部分 | 故障排查诊断 | 5分钟 | - | 第12部分 | 清理和总结 | 2分钟 | - -### 高级话题 - -6. **[ENCRYPTION.md](./ENCRYPTION.md)** - ETCD 加密完整指南 - - 🔑 第1部分:密钥生成 - - 📋 第2部分:配置格式 - - 🔧 第3部分:kube-apiserver 修改 - - ✅第4部分:验证加密 - - ⚠️ 第5部分:关键警告(数据不可恢复) - - 🔐 第6部分:RBAC 解释 - - 📦 第7部分:Deployment 示例 - - 🍎 第8部分:Minikube 特定说明 - -7. **[INTEGRATION.md](../api/INTEGRATION.md)** - 代码集成指南 - - 🔗 第1部分:gRPC Unary Interceptor - - 🔗 第2部分:gRPC Stream Interceptor - - 👤 第3部分:登录 Handler 实现 - - 🔐 第4部分:受保护 Handler 中的声明提取 - - 🔄 第5部分:令牌刷新端点 - - 🚪 第6部分:登出处理 - - 🛣️ 第7部分:REST Routes 配置 - - 🔎 第8部分:错误处理最佳实践 - - 🧪 第9部分:单元测试示例 - ---- - -## 📁 文件结构详解 - -``` -deploy/k8s/ -├── secrets/ -│ ├── jwt-secret.yaml ✅ Kubernetes 清单文件 -│ │ ├── Secret: jwt-secret (JWT 秘钥数据) -│ │ ├── ServiceAccount: user-rpc -│ │ ├── ServiceAccount: envoy-gateway -│ │ ├── Role: jwt-secret-reader -│ │ ├── RoleBinding: jwt-secret-reader-user-rpc -│ │ └── RoleBinding: jwt-secret-reader-envoy-gateway -│ │ -│ ├── README.md 📖 快速参考指南(5分钟入门) -│ ├── SUMMARY.md 📊 系统概览(10分钟了解全貌) -│ ├── QUICK_REFERENCE.md ⚡ 速查表(查找命令和参数) -│ ├── DEPLOYMENT.md 📦 详细部署指南(60分钟完整部署) -│ ├── ENCRYPTION.md 🔐 ETCD 加密指南(Control Plane 配置) -│ ├── VERIFICATION.md ✅ 验证清单(部署后验证) -│ └── INDEX.md 🗺️ 本文件(文档导航) -│ -└── envoy/ -│ └── envoy.yaml ✅ Envoy 网关配置 -│ └── 已更新: serviceAccountName: envoy-gateway -│ -service/user/ -├── user-api.yaml ✅ user-api Service -├── user-rpc.yaml ✅ user-rpc Deployment(已更新) -│ ├── serviceAccountName: user-rpc (已更新) -│ ├── JWT_SECRET_KEY env var (已更新) -│ └── Redis Cluster configuration -└── ... - -app/users/ -├── api/ -│ └── INTEGRATION.md 📝 REST/gRPC 集成指南 -│ -└── rpc/ - ├── internal/utils/jwt.go ✅ JwtManager 实现(已存在) - ├── internal/config/config.go ✅ JWT 配置(已存在) - ├── internal/svc/ - │ └── serviceContext.go ✅ 依赖注入(已存在) - └── etc/pb.yaml ✅ 运行时配置(已存在) -``` - ---- - -## 🎯 按场景查找文档 - -### 场景 1:我想快速了解这个系统是什么 - -**推荐阅读顺序:** -1. [SUMMARY.md](./SUMMARY.md) - 项目概览(5分钟) -2. [SUMMARY.md](./SUMMARY.md) 中的架构图和特性说明 - -**关键信息:** -- JWT 令牌系统 + Redis 存储 + RBAC 权限 + ETCD 加密 -- 支持 7 天有效期、30 天可刷新 -- Envoy 网关 CSRF 防护 - ---- - -### 场景 2:我想立即部署到 Kubernetes - -**推荐阅读顺序:** -1. [README.md](./README.md) - 快速参考(2分钟) -2. [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) - 复制粘贴命令(3分钟) -3. 运行部署命令(5分钟) -4. [VERIFICATION.md](./VERIFICATION.md) 第1-7部分 - 验证(10分钟) - -**快速命令:** -```bash -# Copy from QUICK_REFERENCE.md "部署命令" 部分 -kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml -kubectl apply -f deploy/k8s/service/user/user-rpc.yaml -kubectl apply -f deploy/k8s/envoy/envoy.yaml -``` - ---- - -### 场景 3:部署后验证一切正常 - -**推荐阅读:** -- [VERIFICATION.md](./VERIFICATION.md) - 12部分完整验证清单 -- 逐部分执行验证命令 - -**预计时间:** 30-40分钟 - -**验证触发点:** -- ✅ Secrets 和 RBAC 已创建 -- ✅ Pods 已启动运行 -- ✅ 权限验证通过 -- ✅ Redis 连接成功 - ---- - -### 场景 4:启用 ETCD 加密(生产推荐) - -**推荐阅读顺序:** -1. [ENCRYPTION.md](./ENCRYPTION.md) - 完整加密指南 -2. 按照 8 个步骤逐一执行 -3. [VERIFICATION.md](./VERIFICATION.md) 第9部分 - 加密验证 - -**需要的权限:** -- Control Plane 节点的 root/sudo 权限 -- Kubernetes 集群管理员权限 - -**预计时间:** 15-20分钟 - ---- - -### 场景 5:集成 JWT 到我的应用代码中 - -**推荐阅读顺序:** -1. [INTEGRATION.md](../api/INTEGRATION.md) 第1-2部分 - gRPC 拦截器 -2. 第3-4部分 - 登录和受保护 Handlers -3. 第7-8部分 - REST API 中间件 -4. 第9部分 - 单元测试 - -**需要实现:** -- ✅ gRPC Unary/Stream Interceptors -- ✅ 登录/登出端点 -- ✅ JWT Middleware for REST -- ✅ 错误处理 - -**预计时间:** 2-3 小时 - ---- - -### 场景 6:部署后遇到问题 - -**根据错误类型选择:** - -| 错误类型 | 查看文档 | -|---------|--------| -| Pod 无法启动 | [VERIFICATION.md](./VERIFICATION.md) 第11部分 | -| 权限被拒绝 | [VERIFICATION.md](./VERIFICATION.md) 第2部分 + [README.md](./README.md) 故障排查 | -| Redis 连接失败 | [VERIFICATION.md](./VERIFICATION.md) 第4部分 | -| ETCD 加密失败 | [ENCRYPTION.md](./ENCRYPTION.md) 第5-6部分 | -| 配置不清楚 | [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 配置文件位置 | - ---- - -### 场景 7:定期维护任务 - -#### 任务:轮换 JWT 秘钥 - -**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 安全最佳实践 > 密钥轮换 -**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 密钥轮换步骤 - -**频率:** 季度(3个月) - -#### 任务:轮换 ETCD 加密密钥 - -**阅读:** [ENCRYPTION.md](./ENCRYPTION.md) 第5部分 -**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) ETCD 加密系统 - -**频率:** 年度(12个月) - -#### 任务:备份密钥 - -**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 灾难恢复 -**或:** [ENCRYPTION.md](./ENCRYPTION.md) 关键警告 - -**频率:** 立即 + 每次轮换后 - ---- - -## 📊 文档深度对比 - -| 文档 | 深度 | 丰富度 | 代码 | 适合角色 | -|-----|------|--------|------|---------| -| README | 浅 | 概览 | - | PM/初学者 | -| SUMMARY | 浅 | 概览 | - | 决策者 | -| QUICK_REFERENCE | 中 | 速查 | 命令 | DevOps/SRE | -| DEPLOYMENT | 深 | 详细 | 示例 | DevOps/运维 | -| VERIFICATION | 深 | 详细 | 脚本 | QA/DevOps | -| ENCRYPTION | 非常深 | 极详细 | YAML | 安全/运维 | -| INTEGRATION | 非常深 | 代码级 | 完整 | 开发者 | - ---- - -## 🔄 学习路径建议 - -### 对于 DevOps/SRE - -1. SUMMARY.md (5 分钟) -2. DEPLOYMENT.md (30 分钟) -3. VERIFICATION.md (30 分钟) -4. ENCRYPTION.md (20 分钟) -5. 实践部署 (60 分钟) - -**总计:** ~3 小时 - -### 对于应用开发者 - -1. SUMMARY.md > "集成点" 部分 (5 分钟) -2. INTEGRATION.md (60 分钟) -3. QUICK_REFERENCE.md > "JWT Manager API" (10 分钟) -4. 代码实现 (2-3 小时) -5. 单元测试 (INTEGRATION.md 第9部分) - -**总计:** ~4 小时 - -### 对于安全/合规人员 - -1. SUMMARY.md (5 分钟) -2. ENCRYPTION.md (30 分钟) -3. DEPLOYMENT.md > 安全最佳实践 (15 分钟) -4. VERIFICATION.md 第9部分 (10 分钟) - -**总计:** ~1 小时 - -### 对于项目经理 - -1. SUMMARY.md (5 分钟) -2. DEPLOYMENT.md > "部署状态示意图" (5 分钟) -3. DEPLOYMENT.md > "快速部署" (2 分钟) - -**总计:** ~15 分钟 - ---- - -## 🎓 学习成果预期 - -### 完成后,您将能够: - -✅ 在 Kubernetes 中部署 JWT 认证系统 -✅ 配置 RBAC 权限控制 -✅ 启用 ETCD 加密保护敏感数据 -✅ 在 Go-zero 应用中集成 JWT -✅ 实现令牌刷新和撤销 -✅ 诊断和排查常见问题 -✅ 执行密钥轮换和灾难恢复 - ---- - -## 🆘 求助指南 - -### 第一步:找到相关文档 -- 浏览本索引找到相关章节 -- 或用 Ctrl+F 搜索关键词 - -### 第二步:查看文档中的相关部分 -- DEPLOYMENT.md 的相关章节 -- 或 VERIFICATION.md 的故障排查部分 - -### 第三步:运行诊断命令 -- QUICK_REFERENCE.md 的 "故障排查" 部分 -- 或 VERIFICATION.md 的 "故障排查" 部分 - -### 第四步:检查日志 -```bash -kubectl logs -n juwan -l app=user-rpc -f -kubectl logs -n juwan -l app=envoy-gateway -f -``` - -### 第五步:查看详细文档 -如果上述步骤未能解决,查看对应的详细文档: -- 配置问题 → DEPLOYMENT.md -- 权限问题 → VERIFICATION.md 第2/11部分 -- 集成问题 → INTEGRATION.md -- 加密问题 → ENCRYPTION.md - ---- - -## 📞 文档反馈 - -如果您发现: -- ❌ 文档不清楚 -- ❌ 命令不工作 -- ❌ 信息缺失或过时 -- ❌ 错别字或格式问题 - -请在相应的 `.md` 文件中标记,或提交更新建议。 - ---- - -## 📌 关键概念快速链接 - -| 概念 | 详见 | -|-----|------| -| JWT 令牌生命周期 | SUMMARY.md "关键特性" | -| Redis 双键结构 | SUMMARY.md "关键特性" | -| RBAC 权限隔离 | SUMMARY.md "关键特性" | -| CSRF 防护 | SUMMARY.md "关键特性" | -| ETCD 加密 | ENCRYPTION.md | -| 错误处理 | INTEGRATION.md 第8部分 | -| 密钥轮换 | DEPLOYMENT.md "安全最佳实践" | -| 灾难恢复 | DEPLOYMENT.md "灾难恢复" | - ---- - -## ✨ 文档特性 - -✅ **模块化** - 每个文档独立,但相互链接 -✅ **分层** - 从快速概览到深度细节 -✅ **实践导向** - 包含实际命令和代码示例 -✅ **完整性** - 覆盖部署、验证、维护、故障排查 -✅ **易查找** - 目录、索引、速查表 - ---- - -**开始阅读:** 👉 [SUMMARY.md](./SUMMARY.md) - -或根据您的角色选择: - -| 角色 | 开始文档 | 预计时间 | -|-----|--------|--------| -| DevOps/运维 | [DEPLOYMENT.md](./DEPLOYMENT.md) | 1-2 小时 | -| 应用开发 | [INTEGRATION.md](../api/INTEGRATION.md) | 2-3 小时 | -| 安全审查 | [ENCRYPTION.md](./ENCRYPTION.md) | 30 分钟 | -| 项目经理 | [SUMMARY.md](./SUMMARY.md) | 15 分钟 | -| 新手 | [README.md](./README.md) → [SUMMARY.md](./SUMMARY.md) | 15-20 分钟 | +# JWT + ETCD 加密系统文档索引 + +## 📚 文档完整导航 + +### 快速入门 (5-15分钟) + +**推荐路径:** 从上到下顺序阅读 + +1. **[SUMMARY.md](./SUMMARY.md)** ⭐ 从这里开始 + - 📋 项目概览和架构图 + - 🎯 核心特性一览 + - ✅ 生产就绪检查清单 + - 🚀 下一步行动计划 + +2. **[README.md](./README.md)** + - 🏃 4个快速部署步骤 + - 🔐 安全考虑事项 + - 🔄 密钥轮换程序 + - 🆘 故障排查 + +3. **[QUICK_REFERENCE.md](./QUICK_REFERENCE.md)** + - ⚡ 一页速查表 + - 📝 常见命令复制粘贴 + - 🗺️ 文档地图 + - 🎓 关键参数速知 + +### 部署实施 (30-60分钟) + +4. **[DEPLOYMENT.md](./DEPLOYMENT.md)** - 最详细的部署指南 + - 📦 第1步:创建 Secret 和 RBAC(必需) + - 🔄 第2步:更新 user-rpc Deployment + - 🌐 第3步:更新 Envoy Gateway Deployment + - 🔐 第4步:启用 ETCD 加密(生产推荐) + - ✔️ 第5步:验证整个系统 + - 📊 监控和日志配置 + - 🛠️ 安全最佳实践 + - 🆘 故障排查指南 + - 💾 灾难恢复流程 + +### 验证和监控 (20-30分钟) + +5. **[VERIFICATION.md](./VERIFICATION.md)** - 完整验证清单 + + **12个验证部分:** + + | 部分 | 用途 | 时间 | + |-----|------|------| + | 第1部分 | Secret/RBAC 基础验证 | 2分钟 | + | 第2部分 | 权限测试(allow/deny) | 3分钟 | + | 第3部分 | Deployment 配置检查 | 2分钟 | + | 第4部分 | Redis 连接测试 | 2分钟 | + | 第5部分 | 应用启动日志 | 3分钟 | + | 第6部分 | 网络和服务发现 | 3分钟 | + | 第7部分 | Prometheus 指标 | 3分钟 | + | 第8部分 | Loki 日志聚合 | 2分钟 | + | 第9部分 | ETCD 加密验证 | 5分钟 | + | 第10部分 | JWT 功能测试 | 10分钟 | + | 第11部分 | 故障排查诊断 | 5分钟 | + | 第12部分 | 清理和总结 | 2分钟 | + +### 高级话题 + +6. **[ENCRYPTION.md](./ENCRYPTION.md)** - ETCD 加密完整指南 + - 🔑 第1部分:密钥生成 + - 📋 第2部分:配置格式 + - 🔧 第3部分:kube-apiserver 修改 + - ✅第4部分:验证加密 + - ⚠️ 第5部分:关键警告(数据不可恢复) + - 🔐 第6部分:RBAC 解释 + - 📦 第7部分:Deployment 示例 + - 🍎 第8部分:Minikube 特定说明 + +7. **[INTEGRATION.md](../api/INTEGRATION.md)** - 代码集成指南 + - 🔗 第1部分:gRPC Unary Interceptor + - 🔗 第2部分:gRPC Stream Interceptor + - 👤 第3部分:登录 Handler 实现 + - 🔐 第4部分:受保护 Handler 中的声明提取 + - 🔄 第5部分:令牌刷新端点 + - 🚪 第6部分:登出处理 + - 🛣️ 第7部分:REST Routes 配置 + - 🔎 第8部分:错误处理最佳实践 + - 🧪 第9部分:单元测试示例 + +--- + +## 📁 文件结构详解 + +``` +deploy/k8s/ +├── secrets/ +│ ├── jwt-secret.yaml ✅ Kubernetes 清单文件 +│ │ ├── Secret: jwt-secret (JWT 秘钥数据) +│ │ ├── ServiceAccount: user-rpc +│ │ ├── ServiceAccount: envoy-gateway +│ │ ├── Role: jwt-secret-reader +│ │ ├── RoleBinding: jwt-secret-reader-user-rpc +│ │ └── RoleBinding: jwt-secret-reader-envoy-gateway +│ │ +│ ├── README.md 📖 快速参考指南(5分钟入门) +│ ├── SUMMARY.md 📊 系统概览(10分钟了解全貌) +│ ├── QUICK_REFERENCE.md ⚡ 速查表(查找命令和参数) +│ ├── DEPLOYMENT.md 📦 详细部署指南(60分钟完整部署) +│ ├── ENCRYPTION.md 🔐 ETCD 加密指南(Control Plane 配置) +│ ├── VERIFICATION.md ✅ 验证清单(部署后验证) +│ └── INDEX.md 🗺️ 本文件(文档导航) +│ +└── envoy/ +│ └── envoy.yaml ✅ Envoy 网关配置 +│ └── 已更新: serviceAccountName: envoy-gateway +│ +service/user/ +├── user-api.yaml ✅ user-api Service +├── user-rpc.yaml ✅ user-rpc Deployment(已更新) +│ ├── serviceAccountName: user-rpc (已更新) +│ ├── JWT_SECRET_KEY env var (已更新) +│ └── Redis Cluster configuration +└── ... + +app/users/ +├── api/ +│ └── INTEGRATION.md 📝 REST/gRPC 集成指南 +│ +└── rpc/ + ├── internal/utils/jwt.go ✅ JwtManager 实现(已存在) + ├── internal/config/config.go ✅ JWT 配置(已存在) + ├── internal/svc/ + │ └── serviceContext.go ✅ 依赖注入(已存在) + └── etc/pb.yaml ✅ 运行时配置(已存在) +``` + +--- + +## 🎯 按场景查找文档 + +### 场景 1:我想快速了解这个系统是什么 + +**推荐阅读顺序:** +1. [SUMMARY.md](./SUMMARY.md) - 项目概览(5分钟) +2. [SUMMARY.md](./SUMMARY.md) 中的架构图和特性说明 + +**关键信息:** +- JWT 令牌系统 + Redis 存储 + RBAC 权限 + ETCD 加密 +- 支持 7 天有效期、30 天可刷新 +- Envoy 网关 CSRF 防护 + +--- + +### 场景 2:我想立即部署到 Kubernetes + +**推荐阅读顺序:** +1. [README.md](./README.md) - 快速参考(2分钟) +2. [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) - 复制粘贴命令(3分钟) +3. 运行部署命令(5分钟) +4. [VERIFICATION.md](./VERIFICATION.md) 第1-7部分 - 验证(10分钟) + +**快速命令:** +```bash +# Copy from QUICK_REFERENCE.md "部署命令" 部分 +kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml +kubectl apply -f deploy/k8s/service/user/user-rpc.yaml +kubectl apply -f deploy/k8s/envoy/envoy.yaml +``` + +--- + +### 场景 3:部署后验证一切正常 + +**推荐阅读:** +- [VERIFICATION.md](./VERIFICATION.md) - 12部分完整验证清单 +- 逐部分执行验证命令 + +**预计时间:** 30-40分钟 + +**验证触发点:** +- ✅ Secrets 和 RBAC 已创建 +- ✅ Pods 已启动运行 +- ✅ 权限验证通过 +- ✅ Redis 连接成功 + +--- + +### 场景 4:启用 ETCD 加密(生产推荐) + +**推荐阅读顺序:** +1. [ENCRYPTION.md](./ENCRYPTION.md) - 完整加密指南 +2. 按照 8 个步骤逐一执行 +3. [VERIFICATION.md](./VERIFICATION.md) 第9部分 - 加密验证 + +**需要的权限:** +- Control Plane 节点的 root/sudo 权限 +- Kubernetes 集群管理员权限 + +**预计时间:** 15-20分钟 + +--- + +### 场景 5:集成 JWT 到我的应用代码中 + +**推荐阅读顺序:** +1. [INTEGRATION.md](../api/INTEGRATION.md) 第1-2部分 - gRPC 拦截器 +2. 第3-4部分 - 登录和受保护 Handlers +3. 第7-8部分 - REST API 中间件 +4. 第9部分 - 单元测试 + +**需要实现:** +- ✅ gRPC Unary/Stream Interceptors +- ✅ 登录/登出端点 +- ✅ JWT Middleware for REST +- ✅ 错误处理 + +**预计时间:** 2-3 小时 + +--- + +### 场景 6:部署后遇到问题 + +**根据错误类型选择:** + +| 错误类型 | 查看文档 | +|---------|--------| +| Pod 无法启动 | [VERIFICATION.md](./VERIFICATION.md) 第11部分 | +| 权限被拒绝 | [VERIFICATION.md](./VERIFICATION.md) 第2部分 + [README.md](./README.md) 故障排查 | +| Redis 连接失败 | [VERIFICATION.md](./VERIFICATION.md) 第4部分 | +| ETCD 加密失败 | [ENCRYPTION.md](./ENCRYPTION.md) 第5-6部分 | +| 配置不清楚 | [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 配置文件位置 | + +--- + +### 场景 7:定期维护任务 + +#### 任务:轮换 JWT 秘钥 + +**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 安全最佳实践 > 密钥轮换 +**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) 密钥轮换步骤 + +**频率:** 季度(3个月) + +#### 任务:轮换 ETCD 加密密钥 + +**阅读:** [ENCRYPTION.md](./ENCRYPTION.md) 第5部分 +**或:** [QUICK_REFERENCE.md](./QUICK_REFERENCE.md) ETCD 加密系统 + +**频率:** 年度(12个月) + +#### 任务:备份密钥 + +**阅读:** [DEPLOYMENT.md](./DEPLOYMENT.md) 灾难恢复 +**或:** [ENCRYPTION.md](./ENCRYPTION.md) 关键警告 + +**频率:** 立即 + 每次轮换后 + +--- + +## 📊 文档深度对比 + +| 文档 | 深度 | 丰富度 | 代码 | 适合角色 | +|-----|------|--------|------|---------| +| README | 浅 | 概览 | - | PM/初学者 | +| SUMMARY | 浅 | 概览 | - | 决策者 | +| QUICK_REFERENCE | 中 | 速查 | 命令 | DevOps/SRE | +| DEPLOYMENT | 深 | 详细 | 示例 | DevOps/运维 | +| VERIFICATION | 深 | 详细 | 脚本 | QA/DevOps | +| ENCRYPTION | 非常深 | 极详细 | YAML | 安全/运维 | +| INTEGRATION | 非常深 | 代码级 | 完整 | 开发者 | + +--- + +## 🔄 学习路径建议 + +### 对于 DevOps/SRE + +1. SUMMARY.md (5 分钟) +2. DEPLOYMENT.md (30 分钟) +3. VERIFICATION.md (30 分钟) +4. ENCRYPTION.md (20 分钟) +5. 实践部署 (60 分钟) + +**总计:** ~3 小时 + +### 对于应用开发者 + +1. SUMMARY.md > "集成点" 部分 (5 分钟) +2. INTEGRATION.md (60 分钟) +3. QUICK_REFERENCE.md > "JWT Manager API" (10 分钟) +4. 代码实现 (2-3 小时) +5. 单元测试 (INTEGRATION.md 第9部分) + +**总计:** ~4 小时 + +### 对于安全/合规人员 + +1. SUMMARY.md (5 分钟) +2. ENCRYPTION.md (30 分钟) +3. DEPLOYMENT.md > 安全最佳实践 (15 分钟) +4. VERIFICATION.md 第9部分 (10 分钟) + +**总计:** ~1 小时 + +### 对于项目经理 + +1. SUMMARY.md (5 分钟) +2. DEPLOYMENT.md > "部署状态示意图" (5 分钟) +3. DEPLOYMENT.md > "快速部署" (2 分钟) + +**总计:** ~15 分钟 + +--- + +## 🎓 学习成果预期 + +### 完成后,您将能够: + +✅ 在 Kubernetes 中部署 JWT 认证系统 +✅ 配置 RBAC 权限控制 +✅ 启用 ETCD 加密保护敏感数据 +✅ 在 Go-zero 应用中集成 JWT +✅ 实现令牌刷新和撤销 +✅ 诊断和排查常见问题 +✅ 执行密钥轮换和灾难恢复 + +--- + +## 🆘 求助指南 + +### 第一步:找到相关文档 +- 浏览本索引找到相关章节 +- 或用 Ctrl+F 搜索关键词 + +### 第二步:查看文档中的相关部分 +- DEPLOYMENT.md 的相关章节 +- 或 VERIFICATION.md 的故障排查部分 + +### 第三步:运行诊断命令 +- QUICK_REFERENCE.md 的 "故障排查" 部分 +- 或 VERIFICATION.md 的 "故障排查" 部分 + +### 第四步:检查日志 +```bash +kubectl logs -n juwan -l app=user-rpc -f +kubectl logs -n juwan -l app=envoy-gateway -f +``` + +### 第五步:查看详细文档 +如果上述步骤未能解决,查看对应的详细文档: +- 配置问题 → DEPLOYMENT.md +- 权限问题 → VERIFICATION.md 第2/11部分 +- 集成问题 → INTEGRATION.md +- 加密问题 → ENCRYPTION.md + +--- + +## 📞 文档反馈 + +如果您发现: +- ❌ 文档不清楚 +- ❌ 命令不工作 +- ❌ 信息缺失或过时 +- ❌ 错别字或格式问题 + +请在相应的 `.md` 文件中标记,或提交更新建议。 + +--- + +## 📌 关键概念快速链接 + +| 概念 | 详见 | +|-----|------| +| JWT 令牌生命周期 | SUMMARY.md "关键特性" | +| Redis 双键结构 | SUMMARY.md "关键特性" | +| RBAC 权限隔离 | SUMMARY.md "关键特性" | +| CSRF 防护 | SUMMARY.md "关键特性" | +| ETCD 加密 | ENCRYPTION.md | +| 错误处理 | INTEGRATION.md 第8部分 | +| 密钥轮换 | DEPLOYMENT.md "安全最佳实践" | +| 灾难恢复 | DEPLOYMENT.md "灾难恢复" | + +--- + +## ✨ 文档特性 + +✅ **模块化** - 每个文档独立,但相互链接 +✅ **分层** - 从快速概览到深度细节 +✅ **实践导向** - 包含实际命令和代码示例 +✅ **完整性** - 覆盖部署、验证、维护、故障排查 +✅ **易查找** - 目录、索引、速查表 + +--- + +**开始阅读:** 👉 [SUMMARY.md](./SUMMARY.md) + +或根据您的角色选择: + +| 角色 | 开始文档 | 预计时间 | +|-----|--------|--------| +| DevOps/运维 | [DEPLOYMENT.md](./DEPLOYMENT.md) | 1-2 小时 | +| 应用开发 | [INTEGRATION.md](../api/INTEGRATION.md) | 2-3 小时 | +| 安全审查 | [ENCRYPTION.md](./ENCRYPTION.md) | 30 分钟 | +| 项目经理 | [SUMMARY.md](./SUMMARY.md) | 15 分钟 | +| 新手 | [README.md](./README.md) → [SUMMARY.md](./SUMMARY.md) | 15-20 分钟 | diff --git a/docs/secrets/QUICK_REFERENCE.md b/docs/secrets/QUICK_REFERENCE.md index 085ebfa..1527b78 100644 --- a/docs/secrets/QUICK_REFERENCE.md +++ b/docs/secrets/QUICK_REFERENCE.md @@ -1,350 +1,350 @@ -# JWT + ETCD 加密系统 - 快速参考卡片 - -## 一页速查表 - -### 部署命令 - -```bash -# 创建 Secret 和 RBAC -kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml - -# 更新 Deployments -kubectl apply -f deploy/k8s/service/user/user-rpc.yaml -kubectl apply -f deploy/k8s/envoy/envoy.yaml - -# 验证部署 -kubectl get secret jwt-secret -n juwan -kubectl get sa user-rpc envoy-gateway -n juwan -kubectl get role jwt-secret-reader -n juwan -kubectl get pods -n juwan -l app=user-rpc -kubectl get pods -n juwan -l app=envoy-gateway -``` - -### 权限验证 - -```bash -# user-rpc 可以读 jwt-secret -kubectl auth can-i get secrets \ - --as=system:serviceaccount:juwan:user-rpc \ - --resource-name=jwt-secret -n juwan -# 预期: yes - -# 其他 SA 无法读 jwt-secret -kubectl auth can-i get secrets \ - --as=system:serviceaccount:juwan:default \ - --resource-name=jwt-secret -n juwan -# 预期: no -``` - -### 日志查看 - -```bash -# user-rpc 日志 -kubectl logs -n juwan -l app=user-rpc -f - -# Envoy 日志 -kubectl logs -n juwan -l app=envoy-gateway -f - -# 特定 Pod 日志 -kubectl logs -n juwan --all-containers=true -f -``` - -### 环境变量验证 - -```bash -# 检查 JWT_SECRET_KEY 已注入 -kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) \ - -n juwan -- env | grep JWT_SECRET_KEY -``` - -### Redis 验证 - -```bash -# 连接到 Redis Cluster -kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \ - redis-cli -h user-redis.juwan:6379 -c CLUSTER INFO - -# 测试键操作 -kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \ - redis-cli -h user-redis.juwan:6379 -c GET jwt:user:test-user-id -``` - -### ETCD 加密配置 - -```bash -# 1. 在 Control Plane 节点生成密钥 -head -c 32 /dev/urandom | base64 - -# 2. 编辑 kube-apiserver 清单 -sudo nano /etc/kubernetes/manifests/kube-apiserver.yaml - -# 添加参数: ---encryption-provider-config=/etc/kubernetes/encryption-config.yaml - -# 3. 创建加密配置文件 -cat < - - identity: {} -EOF - -# 4. 验证加密 -kubectl create secret generic test-encryption -n juwan --from-literal=key=value -sudo ETCDCTL_API=3 etcdctl --cert=/etc/kubernetes/pki/etcd/server.crt \ - --key=/etc/kubernetes/pki/etcd/server.key \ - --cacert=/etc/kubernetes/pki/etcd/ca.crt \ - --endpoints=127.0.0.1:2379 \ - get /registry/secrets/juwan/test-encryption | od -A x -t x1z -``` - -### 故障排查 - -```bash -# Pod 无法启动?查看事件 -kubectl describe pod -n juwan - -# 权限被拒绝?检查 RBAC -kubectl get rolebinding -n juwan -o wide -kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan - -# 无法挂载 Secret?检查 Secret 存在性 -kubectl get secret jwt-secret -n juwan -o yaml - -# Redis 连接错误?测试连通性 -kubectl exec -it -n juwan -- \ - redis-cli -h user-redis.juwan:6379 PING -``` - -## JWT Manager API 速查 - -### JwtManager 方法 - -```go -// 生成新令牌 -token, err := svcCtx.JwtManager.New(ctx, userID, email, name) - -// 验证令牌 -claims, err := svcCtx.JwtManager.Valid(ctx, token) - -// 刷新令牌(如果过期但 Redis 仍有数据) -newToken, err := svcCtx.JwtManager.Renew(ctx, token) - -// 提取声明(不验证签名) -claims, err := svcCtx.JwtManager.Extract(ctx, token) - -// 检查令牌是否存在于 Redis -exists, err := svcCtx.JwtManager.Exists(ctx, token) - -// 撤销令牌(登出) -err := svcCtx.JwtManager.Revoke(ctx, userID, token) - -// 获取用户当前令牌 -token, err := svcCtx.JwtManager.GetUserToken(ctx, userID) - -// 将声明转换为载荷 -payload := svcCtx.JwtManager.ClaimsToPayload(claims) -``` - -## 配置文件位置 - -| 配置 | 位置 | 关键参数 | -|-----|------|--------| -| JWT Secret | `deploy/k8s/secrets/jwt-secret.yaml` | `secret-key` | -| user-rpc 配置 | `app/users/rpc/etc/pb.yaml` | `JWT.SecretKey`, `REDIS_HOST` | -| Envoy 配置 | `deploy/k8s/envoy/envoy.yaml` | CSRF 验证 Lua 代码 | -| ETCD 加密 | `/etc/kubernetes/encryption-config.yaml`(Control Plane) | `secret` (32字节密钥) | - -## 关键参数速查 - -```yaml -# JWT 令牌有效期 -Token Exp: 7 days - -# Redis 存储 TTL -Redis TTL: 30 days - -# 可刷新时间窗口 -Refresh Window: 30 days - 7 days = 23 days - -# CSRF Token 位置 -Cookie: csrf_token=... -Header: X-CSRF-Token: ... - -# ETCD 加密算法 -Algorithm: AES-CBC -Key Size: 256 bits (32 bytes) -Encoding: Base64 - -# Secret 挂载方式 -Method: volumeMount (read-only) -或 -Method: valueFrom.secretKeyRef -``` - -## 常见问题速查 - -| 问题 | 排查命令 | 解决方案 | -|-----|--------|--------| -| Pod 无法启动 | `kubectl describe pod` | 检查 Secret/RBAC | -| 权限被拒绝 | `kubectl auth can-i get secrets` | 验证 RBAC 绑定 | -| Redis 连接失败 | `redis-cli PING` | 检查 Redis Pods | -| JWT 验证失败 | 查看 Pod 日志 | 检查 Redis 中的令牌 | -| CSRF 验证失败 | 查看 Envoy 日志 | 检查 Cookie/Header 匹配 | -| ETCD 加密失败 | `kubectl get secret` | 检查 kube-apiserver 启动参数 | - -## 部署检查清单 (5分钟版) - -```bash -# 第1步: 部署 Secret (10秒) -kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml && sleep 5 - -# 第2步: 验证 Secret (10秒) -kubectl get secret jwt-secret -n juwan && echo "✓ Secret 已创建" - -# 第3步: 验证 RBAC (10秒) -kubectl get role jwt-secret-reader -n juwan && echo "✓ RBAC 已配置" - -# 第4步: 更新 Deployments (20秒) -kubectl apply -f deploy/k8s/service/user/user-rpc.yaml -kubectl apply -f deploy/k8s/envoy/envoy.yaml - -# 第5步: 等待 Pods 启动 (30秒) -kubectl rollout status deployment/user-rpc -n juwan -kubectl rollout status deployment/envoy-gateway -n juwan - -# 第6步: 快速功能测试 (2分钟) -# 创建一个令牌并验证可读取 -REDIS_POD=$(kubectl get pod -n juwan -l redis=user-redis -o name | head -1) -kubectl exec -it $REDIS_POD -n juwan -- redis-cli KEYS "jwt:*" -``` - -## 密钥轮换步骤 - -```bash -# 1. 生成新密钥 -NEW_KEY=$(head -c 32 /dev/urandom | base64) - -# 2. 更新 Secret -kubectl create secret generic jwt-secret \ - --from-literal=secret-key=$NEW_KEY \ - --dry-run=client -o yaml | kubectl apply -f - - -# 3. 重启 Pods(自动挂载新 Secret) -kubectl rollout restart deployment/user-rpc -n juwan -kubectl rollout restart deployment/envoy-gateway -n juwan - -# 4. 等待 Pods 启动 -kubectl rollout status deployment/user-rpc -n juwan -kubectl rollout status deployment/envoy-gateway -n juwan - -# 5. 旧令牌现在需要刷新或重新登录 -``` - -## 文档地图 - -``` -deploy/k8s/secrets/ -├── jwt-secret.yaml ← Secrets + RBAC 配置 -├── README.md ← 开始阅读(快速指南) -├── SUMMARY.md ← 本文件(系统概览) -├── DEPLOYMENT.md ← 详细部署步骤(12步) -├── ENCRYPTION.md ← ETCD 加密详细指南 -├── VERIFICATION.md ← 完整验证清单(12部分) -└── QUICK_REFERENCE.md ← 本快速参考卡片 - -app/users/api/ -└── INTEGRATION.md ← JWT 代码集成指南 - -app/users/rpc/ -├── internal/utils/jwt.go ← JwtManager 实现 -├── internal/config/config.go ← JWT 配置 -├── internal/svc/serviceContext.go ← 依赖注入 -└── etc/pb.yaml ← 运行时配置 -``` - -## 关键时间点 - -| 阶段 | 时间 | 操作 | -|-----|------|------| -| 令牌签发 | T0 | 生成 JWT,过期时间 = T0 + 7天 | -| | | 在 Redis 存储,TTL = 30天 | -| Token 过期 | T0 + 7天 | JWT 验证失败 | -| 令牌刷新 | T0 + 7天到T0 + 30天 | 如果 Redis 仍有数据,生成新令牌 | -| 完全失效 | T0 + 30天 | Redis 删除,无法再刷新 | -| 重新登录 | T0 + 30天+ | 用户需要重新登录 | - -## 性能提示 - -```bash -# 高并发下优化 Redis 连接 -# 在 pb.yaml 中调整: -CacheConf: - - Host: "user-redis.juwan:6379" - Type: "cluster" - MaxConnections: 100 - ConnectionPoolSize: 50 - -# 监控 JWT 验证吞吐量 -# 在 Prometheus 查询: -rate(jwt_validations_total[5m]) -rate(jwt_refresh_total[5m]) -``` - -## 安全提示 - -✅ **必做** -- [ ] 定期轮换 JWT 秘钥(季度) -- [ ] 定期轮换 ETCD 加密密钥(年度) -- [ ] 备份加密密钥到安全位置 -- [ ] 启用审计日志 -- [ ] 监控异常的令牌验证失败 - -❌ **禁止** -- [ ] 不要在日志中输出 JWT 秘钥 -- [ ] 不要在代码库中存储密钥 -- [ ] 不要发送明文密钥到 Slack/Email -- [ ] 不要在多个环境间共享密钥 - -## 版本信息 - -``` -Kubernetes: 1.24+ -Go-zero: v1.10.0+ -Redis: 7.0+ -ETCD: 3.5+ -Envoy: v1.32.2+ -``` - -## 支持和反馈 - -遇到问题?按优先级检查: - -1. **运行验证脚本** - ```bash - chmod +x deploy/k8s/secrets/verify-jwt-setup.sh - ./deploy/k8s/secrets/verify-jwt-setup.sh - ``` - -2. **查看日志** - ```bash - kubectl logs -n juwan -l app=user-rpc -f - ``` - -3. **阅读 VERIFICATION.md** - - 第1-5部分: 基础配置 - - 第6-8部分: 网络和监控 - - 第9部分: ETCD 加密 - - 第11部分: 故障排查 - -4. **详细指南** - - DEPLOYMENT.md - 完整步骤 - - INTEGRATION.md - 代码集成 - - ENCRYPTION.md - 加密配置 +# JWT + ETCD 加密系统 - 快速参考卡片 + +## 一页速查表 + +### 部署命令 + +```bash +# 创建 Secret 和 RBAC +kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml + +# 更新 Deployments +kubectl apply -f deploy/k8s/service/user/user-rpc.yaml +kubectl apply -f deploy/k8s/envoy/envoy.yaml + +# 验证部署 +kubectl get secret jwt-secret -n juwan +kubectl get sa user-rpc envoy-gateway -n juwan +kubectl get role jwt-secret-reader -n juwan +kubectl get pods -n juwan -l app=user-rpc +kubectl get pods -n juwan -l app=envoy-gateway +``` + +### 权限验证 + +```bash +# user-rpc 可以读 jwt-secret +kubectl auth can-i get secrets \ + --as=system:serviceaccount:juwan:user-rpc \ + --resource-name=jwt-secret -n juwan +# 预期: yes + +# 其他 SA 无法读 jwt-secret +kubectl auth can-i get secrets \ + --as=system:serviceaccount:juwan:default \ + --resource-name=jwt-secret -n juwan +# 预期: no +``` + +### 日志查看 + +```bash +# user-rpc 日志 +kubectl logs -n juwan -l app=user-rpc -f + +# Envoy 日志 +kubectl logs -n juwan -l app=envoy-gateway -f + +# 特定 Pod 日志 +kubectl logs -n juwan --all-containers=true -f +``` + +### 环境变量验证 + +```bash +# 检查 JWT_SECRET_KEY 已注入 +kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) \ + -n juwan -- env | grep JWT_SECRET_KEY +``` + +### Redis 验证 + +```bash +# 连接到 Redis Cluster +kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \ + redis-cli -h user-redis.juwan:6379 -c CLUSTER INFO + +# 测试键操作 +kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \ + redis-cli -h user-redis.juwan:6379 -c GET jwt:user:test-user-id +``` + +### ETCD 加密配置 + +```bash +# 1. 在 Control Plane 节点生成密钥 +head -c 32 /dev/urandom | base64 + +# 2. 编辑 kube-apiserver 清单 +sudo nano /etc/kubernetes/manifests/kube-apiserver.yaml + +# 添加参数: +--encryption-provider-config=/etc/kubernetes/encryption-config.yaml + +# 3. 创建加密配置文件 +cat < + - identity: {} +EOF + +# 4. 验证加密 +kubectl create secret generic test-encryption -n juwan --from-literal=key=value +sudo ETCDCTL_API=3 etcdctl --cert=/etc/kubernetes/pki/etcd/server.crt \ + --key=/etc/kubernetes/pki/etcd/server.key \ + --cacert=/etc/kubernetes/pki/etcd/ca.crt \ + --endpoints=127.0.0.1:2379 \ + get /registry/secrets/juwan/test-encryption | od -A x -t x1z +``` + +### 故障排查 + +```bash +# Pod 无法启动?查看事件 +kubectl describe pod -n juwan + +# 权限被拒绝?检查 RBAC +kubectl get rolebinding -n juwan -o wide +kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan + +# 无法挂载 Secret?检查 Secret 存在性 +kubectl get secret jwt-secret -n juwan -o yaml + +# Redis 连接错误?测试连通性 +kubectl exec -it -n juwan -- \ + redis-cli -h user-redis.juwan:6379 PING +``` + +## JWT Manager API 速查 + +### JwtManager 方法 + +```go +// 生成新令牌 +token, err := svcCtx.JwtManager.New(ctx, userID, email, name) + +// 验证令牌 +claims, err := svcCtx.JwtManager.Valid(ctx, token) + +// 刷新令牌(如果过期但 Redis 仍有数据) +newToken, err := svcCtx.JwtManager.Renew(ctx, token) + +// 提取声明(不验证签名) +claims, err := svcCtx.JwtManager.Extract(ctx, token) + +// 检查令牌是否存在于 Redis +exists, err := svcCtx.JwtManager.Exists(ctx, token) + +// 撤销令牌(登出) +err := svcCtx.JwtManager.Revoke(ctx, userID, token) + +// 获取用户当前令牌 +token, err := svcCtx.JwtManager.GetUserToken(ctx, userID) + +// 将声明转换为载荷 +payload := svcCtx.JwtManager.ClaimsToPayload(claims) +``` + +## 配置文件位置 + +| 配置 | 位置 | 关键参数 | +|-----|------|--------| +| JWT Secret | `deploy/k8s/secrets/jwt-secret.yaml` | `secret-key` | +| user-rpc 配置 | `app/users/rpc/etc/pb.yaml` | `JWT.SecretKey`, `REDIS_HOST` | +| Envoy 配置 | `deploy/k8s/envoy/envoy.yaml` | CSRF 验证 Lua 代码 | +| ETCD 加密 | `/etc/kubernetes/encryption-config.yaml`(Control Plane) | `secret` (32字节密钥) | + +## 关键参数速查 + +```yaml +# JWT 令牌有效期 +Token Exp: 7 days + +# Redis 存储 TTL +Redis TTL: 30 days + +# 可刷新时间窗口 +Refresh Window: 30 days - 7 days = 23 days + +# CSRF Token 位置 +Cookie: csrf_token=... +Header: X-CSRF-Token: ... + +# ETCD 加密算法 +Algorithm: AES-CBC +Key Size: 256 bits (32 bytes) +Encoding: Base64 + +# Secret 挂载方式 +Method: volumeMount (read-only) +或 +Method: valueFrom.secretKeyRef +``` + +## 常见问题速查 + +| 问题 | 排查命令 | 解决方案 | +|-----|--------|--------| +| Pod 无法启动 | `kubectl describe pod` | 检查 Secret/RBAC | +| 权限被拒绝 | `kubectl auth can-i get secrets` | 验证 RBAC 绑定 | +| Redis 连接失败 | `redis-cli PING` | 检查 Redis Pods | +| JWT 验证失败 | 查看 Pod 日志 | 检查 Redis 中的令牌 | +| CSRF 验证失败 | 查看 Envoy 日志 | 检查 Cookie/Header 匹配 | +| ETCD 加密失败 | `kubectl get secret` | 检查 kube-apiserver 启动参数 | + +## 部署检查清单 (5分钟版) + +```bash +# 第1步: 部署 Secret (10秒) +kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml && sleep 5 + +# 第2步: 验证 Secret (10秒) +kubectl get secret jwt-secret -n juwan && echo "✓ Secret 已创建" + +# 第3步: 验证 RBAC (10秒) +kubectl get role jwt-secret-reader -n juwan && echo "✓ RBAC 已配置" + +# 第4步: 更新 Deployments (20秒) +kubectl apply -f deploy/k8s/service/user/user-rpc.yaml +kubectl apply -f deploy/k8s/envoy/envoy.yaml + +# 第5步: 等待 Pods 启动 (30秒) +kubectl rollout status deployment/user-rpc -n juwan +kubectl rollout status deployment/envoy-gateway -n juwan + +# 第6步: 快速功能测试 (2分钟) +# 创建一个令牌并验证可读取 +REDIS_POD=$(kubectl get pod -n juwan -l redis=user-redis -o name | head -1) +kubectl exec -it $REDIS_POD -n juwan -- redis-cli KEYS "jwt:*" +``` + +## 密钥轮换步骤 + +```bash +# 1. 生成新密钥 +NEW_KEY=$(head -c 32 /dev/urandom | base64) + +# 2. 更新 Secret +kubectl create secret generic jwt-secret \ + --from-literal=secret-key=$NEW_KEY \ + --dry-run=client -o yaml | kubectl apply -f - + +# 3. 重启 Pods(自动挂载新 Secret) +kubectl rollout restart deployment/user-rpc -n juwan +kubectl rollout restart deployment/envoy-gateway -n juwan + +# 4. 等待 Pods 启动 +kubectl rollout status deployment/user-rpc -n juwan +kubectl rollout status deployment/envoy-gateway -n juwan + +# 5. 旧令牌现在需要刷新或重新登录 +``` + +## 文档地图 + +``` +deploy/k8s/secrets/ +├── jwt-secret.yaml ← Secrets + RBAC 配置 +├── README.md ← 开始阅读(快速指南) +├── SUMMARY.md ← 本文件(系统概览) +├── DEPLOYMENT.md ← 详细部署步骤(12步) +├── ENCRYPTION.md ← ETCD 加密详细指南 +├── VERIFICATION.md ← 完整验证清单(12部分) +└── QUICK_REFERENCE.md ← 本快速参考卡片 + +app/users/api/ +└── INTEGRATION.md ← JWT 代码集成指南 + +app/users/rpc/ +├── internal/utils/jwt.go ← JwtManager 实现 +├── internal/config/config.go ← JWT 配置 +├── internal/svc/serviceContext.go ← 依赖注入 +└── etc/pb.yaml ← 运行时配置 +``` + +## 关键时间点 + +| 阶段 | 时间 | 操作 | +|-----|------|------| +| 令牌签发 | T0 | 生成 JWT,过期时间 = T0 + 7天 | +| | | 在 Redis 存储,TTL = 30天 | +| Token 过期 | T0 + 7天 | JWT 验证失败 | +| 令牌刷新 | T0 + 7天到T0 + 30天 | 如果 Redis 仍有数据,生成新令牌 | +| 完全失效 | T0 + 30天 | Redis 删除,无法再刷新 | +| 重新登录 | T0 + 30天+ | 用户需要重新登录 | + +## 性能提示 + +```bash +# 高并发下优化 Redis 连接 +# 在 pb.yaml 中调整: +CacheConf: + - Host: "user-redis.juwan:6379" + Type: "cluster" + MaxConnections: 100 + ConnectionPoolSize: 50 + +# 监控 JWT 验证吞吐量 +# 在 Prometheus 查询: +rate(jwt_validations_total[5m]) +rate(jwt_refresh_total[5m]) +``` + +## 安全提示 + +✅ **必做** +- [ ] 定期轮换 JWT 秘钥(季度) +- [ ] 定期轮换 ETCD 加密密钥(年度) +- [ ] 备份加密密钥到安全位置 +- [ ] 启用审计日志 +- [ ] 监控异常的令牌验证失败 + +❌ **禁止** +- [ ] 不要在日志中输出 JWT 秘钥 +- [ ] 不要在代码库中存储密钥 +- [ ] 不要发送明文密钥到 Slack/Email +- [ ] 不要在多个环境间共享密钥 + +## 版本信息 + +``` +Kubernetes: 1.24+ +Go-zero: v1.10.0+ +Redis: 7.0+ +ETCD: 3.5+ +Envoy: v1.32.2+ +``` + +## 支持和反馈 + +遇到问题?按优先级检查: + +1. **运行验证脚本** + ```bash + chmod +x deploy/k8s/secrets/verify-jwt-setup.sh + ./deploy/k8s/secrets/verify-jwt-setup.sh + ``` + +2. **查看日志** + ```bash + kubectl logs -n juwan -l app=user-rpc -f + ``` + +3. **阅读 VERIFICATION.md** + - 第1-5部分: 基础配置 + - 第6-8部分: 网络和监控 + - 第9部分: ETCD 加密 + - 第11部分: 故障排查 + +4. **详细指南** + - DEPLOYMENT.md - 完整步骤 + - INTEGRATION.md - 代码集成 + - ENCRYPTION.md - 加密配置 diff --git a/docs/secrets/README.md b/docs/secrets/README.md index 7a4ed4b..53ec9ed 100644 --- a/docs/secrets/README.md +++ b/docs/secrets/README.md @@ -1,148 +1,148 @@ -# JWT Secret Management - -This directory contains secure configuration for JWT secret key management. - -## Files - -- `jwt-secret.yaml`: Kubernetes Secret + ServiceAccount + RBAC rules -- `ENCRYPTION.md`: Guide for enabling ETCD static encryption at rest - -## Quick Start - -### 1. Create the Secret and RBAC - -```bash -kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml -``` - -This will create: -- Secret `jwt-secret` in namespace `juwan` containing the JWT secret key -- ServiceAccount `user-rpc` in namespace `juwan` -- ServiceAccount `envoy-gateway` in namespace `juwan` -- Role `jwt-secret-reader` that allows reading only `jwt-secret` -- RoleBindings to grant both ServiceAccounts read permission on the secret - -### 2. Update user-rpc Deployment - -Update `deploy/k8s/service/user/user-rpc.yaml` to: - -1. Set the serviceAccountName: -```yaml -spec: - template: - spec: - serviceAccountName: user-rpc -``` - -2. Add environment variable to load JWT secret: -```yaml -spec: - template: - spec: - containers: - - name: user-rpc - env: - - name: JWT_SECRET_KEY - valueFrom: - secretKeyRef: - name: jwt-secret - key: secret-key -``` - -### 3. Update envoy-gateway Deployment - -Update `deploy/k8s/envoy/envoy.yaml` to: - -1. Set the serviceAccountName: -```yaml -spec: - template: - spec: - serviceAccountName: envoy-gateway -``` - -2. Add environment variable or mount Secret: -```yaml -volumeMounts: -- name: jwt-secret - mountPath: /etc/jwt - readOnly: true - -volumes: -- name: jwt-secret - secret: - secretName: jwt-secret - defaultMode: 0400 -``` - -Then reference it in the Envoy config: -```yaml -data: - envoy.yaml: | - # Read JWT secret from /etc/jwt/secret-key -``` - -### 4. Enable ETCD Encryption - -Follow the guide in `ENCRYPTION.md` to enable static encryption at rest for all secrets in ETCD. - -## Security Considerations - -### Least Privilege - -- Only `user-rpc` and `envoy-gateway` can read the JWT secret -- No other services or users have access -- The Role allows reading **only** the `jwt-secret`, not other secrets - -### Encryption at Rest - -- With ETCD encryption enabled, the secret is encrypted when stored on disk -- Even if someone gains access to the ETCD database files, they cannot read the secret without the encryption key - -### Secret Rotation - -To rotate the JWT secret key: - -1. Generate a new key -2. Update the Secret: - ```bash - kubectl create secret generic jwt-secret --from-literal=secret-key=NEW_KEY --dry-run=client -o yaml | kubectl apply -f - - ``` -3. Pod mounts/env vars will be updated automatically within a few minutes -4. Old tokens will become invalid (you may need to log users out) - -## Production Checklist - -- [ ] ETCD encryption enabled (see ENCRYPTION.md) -- [ ] JWT secret key changed from default -- [ ] Both user-rpc and envoy-gateway Deployments use correct serviceAccountName -- [ ] Both Deployments load the secret via environment variable or volume mount -- [ ] Regular secret rotation policy implemented -- [ ] Secret backup stored in secure location (encrypted) -- [ ] RBAC audit logging enabled to track secret access - -## Troubleshooting - -### Cannot read jwt-secret -Check if the Pod is using the correct ServiceAccount: -```bash -kubectl get deployment user-rpc -o yaml | grep serviceAccountName -``` - -### Secret not being mounted -Verify the Secret exists: -```bash -kubectl get secret jwt-secret -n juwan -``` - -Check Pod logs for mounting errors: -```bash -kubectl logs -l app=user-rpc -n juwan -``` - -### Permission denied error -Verify RBAC binding: -```bash -kubectl get rolebinding -n juwan -kubectl get role jwt-secret-reader -n juwan -``` +# JWT Secret Management + +This directory contains secure configuration for JWT secret key management. + +## Files + +- `jwt-secret.yaml`: Kubernetes Secret + ServiceAccount + RBAC rules +- `ENCRYPTION.md`: Guide for enabling ETCD static encryption at rest + +## Quick Start + +### 1. Create the Secret and RBAC + +```bash +kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml +``` + +This will create: +- Secret `jwt-secret` in namespace `juwan` containing the JWT secret key +- ServiceAccount `user-rpc` in namespace `juwan` +- ServiceAccount `envoy-gateway` in namespace `juwan` +- Role `jwt-secret-reader` that allows reading only `jwt-secret` +- RoleBindings to grant both ServiceAccounts read permission on the secret + +### 2. Update user-rpc Deployment + +Update `deploy/k8s/service/user/user-rpc.yaml` to: + +1. Set the serviceAccountName: +```yaml +spec: + template: + spec: + serviceAccountName: user-rpc +``` + +2. Add environment variable to load JWT secret: +```yaml +spec: + template: + spec: + containers: + - name: user-rpc + env: + - name: JWT_SECRET_KEY + valueFrom: + secretKeyRef: + name: jwt-secret + key: secret-key +``` + +### 3. Update envoy-gateway Deployment + +Update `deploy/k8s/envoy/envoy.yaml` to: + +1. Set the serviceAccountName: +```yaml +spec: + template: + spec: + serviceAccountName: envoy-gateway +``` + +2. Add environment variable or mount Secret: +```yaml +volumeMounts: +- name: jwt-secret + mountPath: /etc/jwt + readOnly: true + +volumes: +- name: jwt-secret + secret: + secretName: jwt-secret + defaultMode: 0400 +``` + +Then reference it in the Envoy config: +```yaml +data: + envoy.yaml: | + # Read JWT secret from /etc/jwt/secret-key +``` + +### 4. Enable ETCD Encryption + +Follow the guide in `ENCRYPTION.md` to enable static encryption at rest for all secrets in ETCD. + +## Security Considerations + +### Least Privilege + +- Only `user-rpc` and `envoy-gateway` can read the JWT secret +- No other services or users have access +- The Role allows reading **only** the `jwt-secret`, not other secrets + +### Encryption at Rest + +- With ETCD encryption enabled, the secret is encrypted when stored on disk +- Even if someone gains access to the ETCD database files, they cannot read the secret without the encryption key + +### Secret Rotation + +To rotate the JWT secret key: + +1. Generate a new key +2. Update the Secret: + ```bash + kubectl create secret generic jwt-secret --from-literal=secret-key=NEW_KEY --dry-run=client -o yaml | kubectl apply -f - + ``` +3. Pod mounts/env vars will be updated automatically within a few minutes +4. Old tokens will become invalid (you may need to log users out) + +## Production Checklist + +- [ ] ETCD encryption enabled (see ENCRYPTION.md) +- [ ] JWT secret key changed from default +- [ ] Both user-rpc and envoy-gateway Deployments use correct serviceAccountName +- [ ] Both Deployments load the secret via environment variable or volume mount +- [ ] Regular secret rotation policy implemented +- [ ] Secret backup stored in secure location (encrypted) +- [ ] RBAC audit logging enabled to track secret access + +## Troubleshooting + +### Cannot read jwt-secret +Check if the Pod is using the correct ServiceAccount: +```bash +kubectl get deployment user-rpc -o yaml | grep serviceAccountName +``` + +### Secret not being mounted +Verify the Secret exists: +```bash +kubectl get secret jwt-secret -n juwan +``` + +Check Pod logs for mounting errors: +```bash +kubectl logs -l app=user-rpc -n juwan +``` + +### Permission denied error +Verify RBAC binding: +```bash +kubectl get rolebinding -n juwan +kubectl get role jwt-secret-reader -n juwan +``` diff --git a/docs/secrets/SUMMARY.md b/docs/secrets/SUMMARY.md index 1e1247d..d63f5b3 100644 --- a/docs/secrets/SUMMARY.md +++ b/docs/secrets/SUMMARY.md @@ -1,366 +1,366 @@ -# JWT 认证系统 + ETCD 加密 - 完整部署总结 - -## 项目概览 - -这个项目为微服务提供了一个完整的 JWT 认证系统,包括: - -1. **JWT 令牌管理** - 令牌生成、验证、刷新和撤销 -2. **Redis Cluster 存储** - 令牌交换缓存(30天TTL)和用户会话管理 -3. **RBAC 权限控制** - 限制只有 user-rpc 和 envoy-gateway 服务可以访问 JWT 秘钥 -4. **ETCD 加密** - 在 Kubernetes 集群中对所有 Secrets 进行加密 -5. **网关保护** - Envoy 网关处理 CSRF 防护和请求路由 - -## 创建的文件清单 - -### 部署配置文件 - -#### `/deploy/k8s/secrets/` - -| 文件 | 说明 | 关键内容 | -|-----|------|--------| -| `jwt-secret.yaml` | Secret + RBAC 配置 | 包含JWT秘钥、ServiceAccounts、Role、RoleBindings | -| `README.md` | 快速参考指南 | Secret 创建和 Deployment 更新说明 | -| `DEPLOYMENT.md` | 详细部署步骤 | 12个部署步骤,包括ETCD加密配置 | -| `ENCRYPTION.md` | ETCD加密完整指南 | 密钥生成、配置修改、验证流程 | -| `VERIFICATION.md` | 验证清单 | 12个部分的完整验证脚本和检查项 | - -### 应用代码更新 - -| 文件路径 | 修改内容 | -|---------|--------| -| `/app/users/rpc/internal/utils/jwt.go` | JwtManager 实现(已存在) | -| `/app/users/rpc/internal/config/config.go` | JwtConfig 结构体(已存在) | -| `/app/users/rpc/internal/svc/serviceContext.go` | Redis Cluster + JwtManager 依赖注入(已存在) | -| `/app/users/rpc/etc/pb.yaml` | JWT 和 Redis Cluster 配置(已存在) | -| `/deploy/k8s/service/user/user-rpc.yaml` | ✅ **已更新** - 添加 serviceAccountName + JWT_SECRET_KEY 环境变量 | -| `/deploy/k8s/envoy/envoy.yaml` | ✅ **已更新** - 添加 serviceAccountName: envoy-gateway | -| `/app/users/api/INTEGRATION.md` | 🆕 **新建** - JWT 集成指南(interceptors, handlers, middleware) | - -## 部署状态示意图 - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Kubernetes Cluster │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ juwan Namespace │ │ -│ ├─────────────────────────────────────────────────────────────┤ │ -│ │ │ │ -│ │ ┌────────────────────────────────────────────────────┐ │ │ -│ │ │ Secret: jwt-secret │ │ │ -│ │ │ ├─ secret-key: │ │ │ -│ │ │ └─ Protected by RBAC Role + RoleBindings │ │ │ -│ │ └────────────────────────────────────────────────────┘ │ │ -│ │ △ │ │ -│ │ │ (mounted via serviceAccountName) │ │ -│ │ │ │ │ -│ │ ┌─────────────────┐ ┌──────────────────────┐ │ │ -│ │ │ user-rpc │ │ envoy-gateway │ │ │ -│ │ │ Deployment │ │ Deployment │ │ │ -│ │ ├─────────────────┤ ├──────────────────────┤ │ │ -│ │ │ SA: user-rpc │ │ SA: envoy-gateway │ │ │ -│ │ │ Replicas: 3 │ │ Replicas: 1 │ │ │ -│ │ │ Port: 9001(RPC) │ │ Port: 8080(HTTP) │ │ │ -│ │ │ 4001(Met) │ │ │ │ │ -│ │ └─────────────────┘ └──────────────────────┘ │ │ -│ │ │ JWT Manager │ CSRF Filter │ │ -│ │ │ (HS256 signing) │ (X-CSRF-Token) │ │ -│ │ │ │ │ │ -│ │ ┌──────▼──────────────────────────▼─────┐ │ │ -│ │ │ user-redis (RedisCluster) │ │ │ -│ │ │ 3-node cluster │ │ │ -│ │ │ - Token exchange cache (30d TTL) │ │ │ -│ │ │ - User session management │ │ │ -│ │ └────────────────────────────────────────┘ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────┐ │ -│ │ Control Plane (kube-apiserver) │ │ -│ │ • ETCD 加密: AES-CBC 32-byte key │ │ -│ │ • Secrets 自动加密存储 │ │ -│ └─────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -## 核心配置参数 - -### JWT 配置 - -```yaml -JWT: - SecretKey: "your-secret-jwt-key-change-this-in-production" - Issuer: "your-app-name" - # Token 有效期: 7 天 - # Redis TTL: 30 天(支持令牌刷新) -``` - -### Redis Cluster - -```yaml -RedisCluster: - ClusterSize: 3 🔴 主服务器 + 2 🔵 从服务器 - Address: "user-redis.juwan:6379" - HighAvailability: 自动故障转移 -``` - -### RBAC 权限 - -```yaml -Role: jwt-secret-reader -Resources: [secrets] -ResourceNames: [jwt-secret] -Verbs: [get] # 只读,无列表/创建/删除 -Subjects: - - user-rpc (ServiceAccount) - - envoy-gateway (ServiceAccount) -``` - -## 部署流程 - -### 快速部署(5分钟) - -```bash -# 第1步:创建 Secret 和 RBAC -kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml - -# 第2步:更新 Deployments -kubectl apply -f deploy/k8s/service/user/user-rpc.yaml -kubectl apply -f deploy/k8s/envoy/envoy.yaml - -# 第3步:验证 -./verify-jwt-setup.sh -``` - -### ETCD 加密部署(需要集群管理员权限) - -```bash -# 在 Control Plane 节点执行 -1. 生成 32 字节密钥 -2. 创建 /etc/kubernetes/encryption-config.yaml -3. 修改 /etc/kubernetes/manifests/kube-apiserver.yaml -4. 重启 kube-apiserver -5. 验证加密已启用 -``` - -详见:`deploy/k8s/secrets/ENCRYPTION.md` - -## 关键特性 - -### 1. JWT 令牌生命周期 - -``` -登录 → 生成 JWT - ├─ 有效期: 7 天(exp claim) - └─ 存储到 Redis: 30 天(TTL) - -Token 过期(7天+) - ├─ JWT 签名验证失败 - └─ 检查 Redis 是否仍有数据 - ├─ 有 → 生成新 Token(刷新)✅ - └─ 无 → 令牌已过期,需要重新登录 ❌ -``` - -### 2. Redis 双键结构 - -``` -jwt:user:{userId} → {token} - 用途: 快速查询用户当前令牌 - TTL: 30 天 - -jwt:token:{token} → {payload} - 用途: 令牌验证和刷新 - TTL: 30 天 -``` - -### 3. CSRF 防护(Envoy 网关) - -``` -安全方法 (GET/HEAD/OPTIONS) - → 自动生成 csrf_token - → 返回 Set-Cookie: csrf_token=... - -不安全方法 (POST/PUT/DELETE/PATCH) - → 检查 Cookie csrf_token - → 检查 X-CSRF-Token 头 - → 两者必须相等,否则 403 -``` - -### 4. 权限隔离 - -``` -Only user-rpc + envoy-gateway 可以: - ✅ 读 jwt-secret - -Other services 无法: - ❌ 列出 secrets - ❌ 获取 jwt-secret(RBAC 拒绝) - ❌ 删除 secrets -``` - -### 5. ETCD 加密 - -``` -未加密: - etcdctl get /registry/seca/... - → secret-key: "plaintext-value" - -已加密 (AES-CBC): - etcdctl get /registry/secrets/... - → 二进制数据,无法读取 -``` - -## 集成点 - -### 1. RPC Handler(需要实现) - -```go -// 在 gRPC server 中注册拦截器 -s := grpc.NewServer( - grpc.UnaryInterceptor(interceptor.JwtUnaryInterceptor(ctx)), -) - -// 拦截器会: -// 1. 提取 Authorization 头中的 Token -// 2. 调用 JwtManager.Valid() -// 3. 如果过期,尝试 JwtManager.Renew() -// 4. 将声明注入 context -``` - -参考:`app/users/api/INTEGRATION.md` 第1-2章 - -### 2. REST Endpoint(需要实现) - -```go -// 创建 JWT Middleware -protected := middleware.JwtMiddleware(svcCtx) - -// 应用到受保护的路由 -router.HandleFunc("GET /api/v1/users/me", - protected(user.GetUserInfoHandler(svcCtx))) - -// Middleware 会验证 Authorization: Bearer {token} -``` - -参考:`app/users/api/INTEGRATION.md` 第3-4章 - -### 3. 登录/登出流程(需要实现) - -``` -登录: - 1. 验证用户凭证(DB 查询) - 2. JwtManager.New() → 生成令牌 - 3. 返回令牌给客户端 - -登出: - 1. 从上下文提取 userId - 2. JwtManager.Revoke() → 删除 Redis 中的令牌 - 3. 用户需要重新登录获取新令牌 -``` - -参考:`app/users/api/INTEGRATION.md` 第5-6章 - -## 文档导航 - -| 场景 | 推荐阅读 | -|-----|--------| -| 第一次部署 | `README.md` → `DEPLOYMENT.md` | -| 部署遇到问题 | `VERIFICATION.md` + `DEPLOYMENT.md` 故障排查部分 | -| 代码集成 | `app/users/api/INTEGRATION.md` | -| ETCD 加密配置 | `ENCRYPTION.md` | -| ETCD 加密验证 | `VERIFICATION.md` 第9部分 | -| 安全最佳实践 | `DEPLOYMENT.md` 安全最佳实践部分 | -| 灾难恢复 | `DEPLOYMENT.md` 灾难恢复部分 | - -## 生产就绪检查清单 - -- [ ] 所有 Pods 都在 Running 状态 -- [ ] JWT Secret 已创建并正确挂载 -- [ ] RBAC 权限验证通过 -- [ ] Redis Cluster 健康(3/3 节点) -- [ ] ETCD 加密已启用(如需要) -- [ ] 监控和日志聚合正常工作 -- [ ] 密钥轮换计划已制定 -- [ ] 备份和恢复流程已文档化 -- [ ] 安全审计日志已启用 -- [ ] 端到端测试已通过 - -## 下一步行动 - -### 短期(本周) - -1. **部署 Secret 和 RBAC** - ```bash - kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml - ``` - -2. **更新 Deployments** - ```bash - kubectl apply -f deploy/k8s/service/user/user-rpc.yaml - kubectl apply -f deploy/k8s/envoy/envoy.yaml - ``` - -3. **验证部署** - ```bash - ./verify-jwt-setup.sh - ``` - -### 中期(本月) - -1. **实现 JWT 集成** - - 创建 gRPC 拦截器 - - 实现登录/登出端点 - - 添加 JWT 中间件到 REST API - -2. **端到端测试** - - 测试令牌生成和验证 - - 测试令牌刷新 - - 测试 CSRF 防护 - -### 长期(本季度) - -1. **启用 ETCD 加密** - - 按照 `ENCRYPTION.md` 配置 - - 验证所有 Secrets 都已加密 - -2. **生产部署** - - 启用审计日志 - - 配置监控和告警 - - 制定密钥轮换政策 - -## 支持 - -如遇到问题: - -1. **检查日志** - ```bash - kubectl logs -n juwan -l app=user-rpc -f - kubectl logs -n juwan -l app=envoy-gateway -f - ``` - -2. **运行验证脚本** - ```bash - chmod +x deploy/k8s/secrets/verify-jwt-setup.sh - ./deploy/k8s/secrets/verify-jwt-setup.sh - ``` - -3. **查看详细文档** - - 部署问题 → `DEPLOYMENT.md` - - 代码集成 → `INTEGRATION.md` - - ETCD 加密 → `ENCRYPTION.md` - - 诊断 → `VERIFICATION.md` - -## 总结 - -这个系统为微服务提供了: - -✅ **安全的身份验证** - JWT 令牌 + HS256 签名 -✅ **灵活的令牌管理** - 7天有效期,30天可刷新 -✅ **高可用性** - Redis Cluster 自动故障转移 -✅ **权限隔离** - RBAC 限制密钥访问 -✅ **数据加密** - ETCD 加密保护敏感信息 -✅ **请求保护** - Envoy CSRF 双令牌验证 - -现在可以部署并集成到应用中了! +# JWT 认证系统 + ETCD 加密 - 完整部署总结 + +## 项目概览 + +这个项目为微服务提供了一个完整的 JWT 认证系统,包括: + +1. **JWT 令牌管理** - 令牌生成、验证、刷新和撤销 +2. **Redis Cluster 存储** - 令牌交换缓存(30天TTL)和用户会话管理 +3. **RBAC 权限控制** - 限制只有 user-rpc 和 envoy-gateway 服务可以访问 JWT 秘钥 +4. **ETCD 加密** - 在 Kubernetes 集群中对所有 Secrets 进行加密 +5. **网关保护** - Envoy 网关处理 CSRF 防护和请求路由 + +## 创建的文件清单 + +### 部署配置文件 + +#### `/deploy/k8s/secrets/` + +| 文件 | 说明 | 关键内容 | +|-----|------|--------| +| `jwt-secret.yaml` | Secret + RBAC 配置 | 包含JWT秘钥、ServiceAccounts、Role、RoleBindings | +| `README.md` | 快速参考指南 | Secret 创建和 Deployment 更新说明 | +| `DEPLOYMENT.md` | 详细部署步骤 | 12个部署步骤,包括ETCD加密配置 | +| `ENCRYPTION.md` | ETCD加密完整指南 | 密钥生成、配置修改、验证流程 | +| `VERIFICATION.md` | 验证清单 | 12个部分的完整验证脚本和检查项 | + +### 应用代码更新 + +| 文件路径 | 修改内容 | +|---------|--------| +| `/app/users/rpc/internal/utils/jwt.go` | JwtManager 实现(已存在) | +| `/app/users/rpc/internal/config/config.go` | JwtConfig 结构体(已存在) | +| `/app/users/rpc/internal/svc/serviceContext.go` | Redis Cluster + JwtManager 依赖注入(已存在) | +| `/app/users/rpc/etc/pb.yaml` | JWT 和 Redis Cluster 配置(已存在) | +| `/deploy/k8s/service/user/user-rpc.yaml` | ✅ **已更新** - 添加 serviceAccountName + JWT_SECRET_KEY 环境变量 | +| `/deploy/k8s/envoy/envoy.yaml` | ✅ **已更新** - 添加 serviceAccountName: envoy-gateway | +| `/app/users/api/INTEGRATION.md` | 🆕 **新建** - JWT 集成指南(interceptors, handlers, middleware) | + +## 部署状态示意图 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Kubernetes Cluster │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ juwan Namespace │ │ +│ ├─────────────────────────────────────────────────────────────┤ │ +│ │ │ │ +│ │ ┌────────────────────────────────────────────────────┐ │ │ +│ │ │ Secret: jwt-secret │ │ │ +│ │ │ ├─ secret-key: │ │ │ +│ │ │ └─ Protected by RBAC Role + RoleBindings │ │ │ +│ │ └────────────────────────────────────────────────────┘ │ │ +│ │ △ │ │ +│ │ │ (mounted via serviceAccountName) │ │ +│ │ │ │ │ +│ │ ┌─────────────────┐ ┌──────────────────────┐ │ │ +│ │ │ user-rpc │ │ envoy-gateway │ │ │ +│ │ │ Deployment │ │ Deployment │ │ │ +│ │ ├─────────────────┤ ├──────────────────────┤ │ │ +│ │ │ SA: user-rpc │ │ SA: envoy-gateway │ │ │ +│ │ │ Replicas: 3 │ │ Replicas: 1 │ │ │ +│ │ │ Port: 9001(RPC) │ │ Port: 8080(HTTP) │ │ │ +│ │ │ 4001(Met) │ │ │ │ │ +│ │ └─────────────────┘ └──────────────────────┘ │ │ +│ │ │ JWT Manager │ CSRF Filter │ │ +│ │ │ (HS256 signing) │ (X-CSRF-Token) │ │ +│ │ │ │ │ │ +│ │ ┌──────▼──────────────────────────▼─────┐ │ │ +│ │ │ user-redis (RedisCluster) │ │ │ +│ │ │ 3-node cluster │ │ │ +│ │ │ - Token exchange cache (30d TTL) │ │ │ +│ │ │ - User session management │ │ │ +│ │ └────────────────────────────────────────┘ │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Control Plane (kube-apiserver) │ │ +│ │ • ETCD 加密: AES-CBC 32-byte key │ │ +│ │ • Secrets 自动加密存储 │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## 核心配置参数 + +### JWT 配置 + +```yaml +JWT: + SecretKey: "your-secret-jwt-key-change-this-in-production" + Issuer: "your-app-name" + # Token 有效期: 7 天 + # Redis TTL: 30 天(支持令牌刷新) +``` + +### Redis Cluster + +```yaml +RedisCluster: + ClusterSize: 3 🔴 主服务器 + 2 🔵 从服务器 + Address: "user-redis.juwan:6379" + HighAvailability: 自动故障转移 +``` + +### RBAC 权限 + +```yaml +Role: jwt-secret-reader +Resources: [secrets] +ResourceNames: [jwt-secret] +Verbs: [get] # 只读,无列表/创建/删除 +Subjects: + - user-rpc (ServiceAccount) + - envoy-gateway (ServiceAccount) +``` + +## 部署流程 + +### 快速部署(5分钟) + +```bash +# 第1步:创建 Secret 和 RBAC +kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml + +# 第2步:更新 Deployments +kubectl apply -f deploy/k8s/service/user/user-rpc.yaml +kubectl apply -f deploy/k8s/envoy/envoy.yaml + +# 第3步:验证 +./verify-jwt-setup.sh +``` + +### ETCD 加密部署(需要集群管理员权限) + +```bash +# 在 Control Plane 节点执行 +1. 生成 32 字节密钥 +2. 创建 /etc/kubernetes/encryption-config.yaml +3. 修改 /etc/kubernetes/manifests/kube-apiserver.yaml +4. 重启 kube-apiserver +5. 验证加密已启用 +``` + +详见:`deploy/k8s/secrets/ENCRYPTION.md` + +## 关键特性 + +### 1. JWT 令牌生命周期 + +``` +登录 → 生成 JWT + ├─ 有效期: 7 天(exp claim) + └─ 存储到 Redis: 30 天(TTL) + +Token 过期(7天+) + ├─ JWT 签名验证失败 + └─ 检查 Redis 是否仍有数据 + ├─ 有 → 生成新 Token(刷新)✅ + └─ 无 → 令牌已过期,需要重新登录 ❌ +``` + +### 2. Redis 双键结构 + +``` +jwt:user:{userId} → {token} + 用途: 快速查询用户当前令牌 + TTL: 30 天 + +jwt:token:{token} → {payload} + 用途: 令牌验证和刷新 + TTL: 30 天 +``` + +### 3. CSRF 防护(Envoy 网关) + +``` +安全方法 (GET/HEAD/OPTIONS) + → 自动生成 csrf_token + → 返回 Set-Cookie: csrf_token=... + +不安全方法 (POST/PUT/DELETE/PATCH) + → 检查 Cookie csrf_token + → 检查 X-CSRF-Token 头 + → 两者必须相等,否则 403 +``` + +### 4. 权限隔离 + +``` +Only user-rpc + envoy-gateway 可以: + ✅ 读 jwt-secret + +Other services 无法: + ❌ 列出 secrets + ❌ 获取 jwt-secret(RBAC 拒绝) + ❌ 删除 secrets +``` + +### 5. ETCD 加密 + +``` +未加密: + etcdctl get /registry/seca/... + → secret-key: "plaintext-value" + +已加密 (AES-CBC): + etcdctl get /registry/secrets/... + → 二进制数据,无法读取 +``` + +## 集成点 + +### 1. RPC Handler(需要实现) + +```go +// 在 gRPC server 中注册拦截器 +s := grpc.NewServer( + grpc.UnaryInterceptor(interceptor.JwtUnaryInterceptor(ctx)), +) + +// 拦截器会: +// 1. 提取 Authorization 头中的 Token +// 2. 调用 JwtManager.Valid() +// 3. 如果过期,尝试 JwtManager.Renew() +// 4. 将声明注入 context +``` + +参考:`app/users/api/INTEGRATION.md` 第1-2章 + +### 2. REST Endpoint(需要实现) + +```go +// 创建 JWT Middleware +protected := middleware.JwtMiddleware(svcCtx) + +// 应用到受保护的路由 +router.HandleFunc("GET /api/v1/users/me", + protected(user.GetUserInfoHandler(svcCtx))) + +// Middleware 会验证 Authorization: Bearer {token} +``` + +参考:`app/users/api/INTEGRATION.md` 第3-4章 + +### 3. 登录/登出流程(需要实现) + +``` +登录: + 1. 验证用户凭证(DB 查询) + 2. JwtManager.New() → 生成令牌 + 3. 返回令牌给客户端 + +登出: + 1. 从上下文提取 userId + 2. JwtManager.Revoke() → 删除 Redis 中的令牌 + 3. 用户需要重新登录获取新令牌 +``` + +参考:`app/users/api/INTEGRATION.md` 第5-6章 + +## 文档导航 + +| 场景 | 推荐阅读 | +|-----|--------| +| 第一次部署 | `README.md` → `DEPLOYMENT.md` | +| 部署遇到问题 | `VERIFICATION.md` + `DEPLOYMENT.md` 故障排查部分 | +| 代码集成 | `app/users/api/INTEGRATION.md` | +| ETCD 加密配置 | `ENCRYPTION.md` | +| ETCD 加密验证 | `VERIFICATION.md` 第9部分 | +| 安全最佳实践 | `DEPLOYMENT.md` 安全最佳实践部分 | +| 灾难恢复 | `DEPLOYMENT.md` 灾难恢复部分 | + +## 生产就绪检查清单 + +- [ ] 所有 Pods 都在 Running 状态 +- [ ] JWT Secret 已创建并正确挂载 +- [ ] RBAC 权限验证通过 +- [ ] Redis Cluster 健康(3/3 节点) +- [ ] ETCD 加密已启用(如需要) +- [ ] 监控和日志聚合正常工作 +- [ ] 密钥轮换计划已制定 +- [ ] 备份和恢复流程已文档化 +- [ ] 安全审计日志已启用 +- [ ] 端到端测试已通过 + +## 下一步行动 + +### 短期(本周) + +1. **部署 Secret 和 RBAC** + ```bash + kubectl apply -f deploy/k8s/secrets/jwt-secret.yaml + ``` + +2. **更新 Deployments** + ```bash + kubectl apply -f deploy/k8s/service/user/user-rpc.yaml + kubectl apply -f deploy/k8s/envoy/envoy.yaml + ``` + +3. **验证部署** + ```bash + ./verify-jwt-setup.sh + ``` + +### 中期(本月) + +1. **实现 JWT 集成** + - 创建 gRPC 拦截器 + - 实现登录/登出端点 + - 添加 JWT 中间件到 REST API + +2. **端到端测试** + - 测试令牌生成和验证 + - 测试令牌刷新 + - 测试 CSRF 防护 + +### 长期(本季度) + +1. **启用 ETCD 加密** + - 按照 `ENCRYPTION.md` 配置 + - 验证所有 Secrets 都已加密 + +2. **生产部署** + - 启用审计日志 + - 配置监控和告警 + - 制定密钥轮换政策 + +## 支持 + +如遇到问题: + +1. **检查日志** + ```bash + kubectl logs -n juwan -l app=user-rpc -f + kubectl logs -n juwan -l app=envoy-gateway -f + ``` + +2. **运行验证脚本** + ```bash + chmod +x deploy/k8s/secrets/verify-jwt-setup.sh + ./deploy/k8s/secrets/verify-jwt-setup.sh + ``` + +3. **查看详细文档** + - 部署问题 → `DEPLOYMENT.md` + - 代码集成 → `INTEGRATION.md` + - ETCD 加密 → `ENCRYPTION.md` + - 诊断 → `VERIFICATION.md` + +## 总结 + +这个系统为微服务提供了: + +✅ **安全的身份验证** - JWT 令牌 + HS256 签名 +✅ **灵活的令牌管理** - 7天有效期,30天可刷新 +✅ **高可用性** - Redis Cluster 自动故障转移 +✅ **权限隔离** - RBAC 限制密钥访问 +✅ **数据加密** - ETCD 加密保护敏感信息 +✅ **请求保护** - Envoy CSRF 双令牌验证 + +现在可以部署并集成到应用中了! diff --git a/docs/secrets/VERIFICATION.md b/docs/secrets/VERIFICATION.md index 16bedf5..7feefe0 100644 --- a/docs/secrets/VERIFICATION.md +++ b/docs/secrets/VERIFICATION.md @@ -1,507 +1,507 @@ -# 完整部署验证清单 - -完成所有部署后使用此清单验证系统是否正确配置和运行。 - -## 第一部分:基础设施验证 - -### Secret 和 RBAC 创建 - -```bash -# 检查 Secret 已创建 -kubectl get secret -n juwan | grep jwt-secret -# 预期输出: jwt-secret Created - -# 查看 Secret 详情(不显示敏感数据) -kubectl describe secret jwt-secret -n juwan -# 应该看到: -# Name: jwt-secret -# Namespace: juwan -# Type: Opaque -# Data -# ==== -# secret-key: - -# 验证 Secret 内容已正确加载 -kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d | wc -c -# 预期输出: 应该是 32 个字符(32 字节密钥的 Base64 解码) -``` - -### ServiceAccount 验证 - -```bash -# 检查 user-rpc ServiceAccount -kubectl get sa user-rpc -n juwan -kubectl describe sa user-rpc -n juwan -# 应该显示正确的 Secrets 挂载 - -# 检查 envoy-gateway ServiceAccount -kubectl get sa envoy-gateway -n juwan -kubectl describe sa envoy-gateway -n juwan -``` - -### RBAC 权限验证 - -```bash -# 检查 Role 定义 -kubectl get role -n juwan -l app=jwt-secret-reader -kubectl describe role jwt-secret-reader -n juwan -# 应该显示: -# PolicyRule: -# Resources Non-Resource URLs Resource Names Verbs -# --------- ----------------- -------------- ----- -# secrets [] [jwt-secret] [get] - -# 检查 RoleBindings -kubectl get rolebinding -n juwan | grep jwt-secret-reader -# 应该显示两个绑定: jwt-secret-reader-user-rpc 和 jwt-secret-reader-envoy-gateway - -# 验证每个 RoleBinding -kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan -kubectl describe rolebinding jwt-secret-reader-envoy-gateway -n juwan -``` - -## 第二部分:权限测试 - -### 权限允许测试 - -```bash -# 测试 user-rpc 可以读 jwt-secret -kubectl auth can-i get secrets \ - --as=system:serviceaccount:juwan:user-rpc \ - --resource-name=jwt-secret \ - -n juwan -# 预期输出: yes - -# 测试 envoy-gateway 可以读 jwt-secret -kubectl auth can-i get secrets \ - --as=system:serviceaccount:juwan:envoy-gateway \ - --resource-name=jwt-secret \ - -n juwan -# 预期输出: yes - -# 测试 user-rpc 无法读其他 Secrets -kubectl auth can-i get secrets \ - --as=system:serviceaccount:juwan:user-rpc \ - -n juwan -# 预期输出: no - -# 测试其他 ServiceAccount 无法读 jwt-secret -kubectl auth can-i get secrets \ - --as=system:serviceaccount:juwan:default \ - --resource-name=jwt-secret \ - -n juwan -# 预期输出: no -``` - -## 第三部分:Deployment 配置验证 - -### user-rpc Deployment 验证 - -```bash -# 检查 ServiceAccountName 是否正确设置 -kubectl get deployment user-rpc -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}' -# 预期输出: user-rpc - -# 检查是否包含所有必需的环境变量 -kubectl get deployment user-rpc -n juwan -o yaml | grep -A 20 "env:" -# 应该包括: -# - name: JWT_SECRET_KEY -# valueFrom: -# secretKeyRef: -# name: jwt-secret -# key: secret-key - -# 检查 Pod 是否正在运行 -kubectl get pods -n juwan -l app=user-rpc -# 应该显示至少 3 个 Running 的 Pod - -# 验证 Pod 已加载 Secret(在 Pod 中执行) -kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep -i jwt -# 应该输出环境变量,例如: -# JWT_SECRET_KEY=your-secret-jwt-key-change-this-in-production -``` - -### Envoy Gateway Deployment 验证 - -```bash -# 检查 ServiceAccountName 是否正确设置 -kubectl get deployment envoy-gateway -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}' -# 预期输出: envoy-gateway - -# 检查 Pod 是否正在运行 -kubectl get pods -n juwan -l app=envoy-gateway -# 应该显示 Running 的 Pod - -# 检查 Envoy 日志 -kubectl logs -n juwan -l app=envoy-gateway -# 应该看到启动日志,没有权限相关错误 -``` - -## 第四部分:Redis 连接验证 - -### Redis Cluster 验证 - -```bash -# 检查 RedisCluster CRD 状态 -kubectl get rediscluster -n juwan -# 应该显示 user-redis,Status 应该是 Healthy - -# 详细查看 RedisCluster 状态 -kubectl describe rediscluster user-redis -n juwan -# 应该显示: -# Status: -# Cluster Status: Healthy -# Nodes Ready: 3/3 -# Master: 1 -# Replicas: 2 - -# 检查 Redis Pods -kubectl get pods -n juwan | grep redis -# 应该显示 3 个 Redis Pod,都在 Running 状态 - -# 测试 Redis 连接 -kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \ - redis-cli -h user-redis.juwan -c CLUSTER INFO -# 应该看到集群信息,cluster_state:ok 表示集群健康 -``` - -## 第五部分:应用启动日志检查 - -### user-rpc 启动日志 - -```bash -# 查看 user-rpc Pods 的启动日志 -kubectl logs -n juwan -l app=user-rpc --all-containers=true - -# 应该包含类似以下消息: -# - "Starting gRPC server on 0.0.0.0:9001" -# - "Redis Cluster connected successfully" 或 JWT Manager 初始化成功 -# - "Listening on metrics port 4001" - -# 如果有错误,查看详细日志 -kubectl logs -n juwan -l app=user-rpc -f --all-containers=true -``` - -### Envoy 启动日志 - -```bash -# 查看 Envoy 启动日志 -kubectl logs -n juwan -l app=envoy-gateway - -# 应该包含: -# - "[info] Configuration: /etc/envoy/envoy.yaml" -# - "[info] listener listening on 0.0.0.0:8080" -# - 没有权限相关错误 -``` - -## 第六部分:网络和服务发现验证 - -### Service 验证 - -```bash -# 检查 user-rpc-svc -kubectl get svc user-rpc-svc -n juwan -# 应该显示 ClusterIP 和两个端口 (9001/rpc 和 4001/metrics) - -# 检查 Envoy Gateway Service -kubectl get svc envoy-gateway -n juwan -# 应该显示 ClusterIP 和端口 80 - -# 检查 Redis Service -kubectl get svc -n juwan | grep redis -# 应该显示 user-redis(ClusterIP)服务 -``` - -### DNS 解析验证 - -```bash -# 测试服务名称解析 -kubectl run -it --rm debug --image=busybox --restart=Never -- \ - nslookup user-rpc-svc.juwan.svc.cluster.local -# 应该返回 ClusterIP 地址 - -kubectl run -it --rm debug --image=busybox --restart=Never -- \ - nslookup user-redis.juwan.svc.cluster.local -# 应该返回 ClusterIP 地址 -``` - -## 第七部分:监控和指标验证 - -### Prometheus 指标收集 - -```bash -# 检查 Prometheus 是否在收集指标 -kubectl port-forward -n monitoring svc/prometheus 9090:9090 & - -# 打开浏览器访问 http://localhost:9090 -# 查看 Status > Targets -# 应该看到 user-rpc-svc:4001 目标显示为 UP - -# 查询一个指标 -curl 'http://localhost:9090/api/v1/query?query=up{job="kubernetes-pods"}' -# 应该返回 user-rpc 的指标数据 - -# 关闭端口转发 -kill %1 -``` - -### 测试源代码级指标端点 - -```bash -# 从 user-rpc Pod 直接访问指标端点 -kubectl port-forward -n juwan svc/user-rpc-svc 4001:4001 & - -# 测试指标端点 -curl http://localhost:4001/metrics - -# 应该看到 Prometheus 格式的指标,例如: -# # HELP go_goroutines Number of goroutines that currently exist. -# # TYPE go_goroutines gauge -# go_goroutines 25 - -# 关闭端口转发 -kill %1 -``` - -## 第八部分:日志聚合验证(Loki) - -```bash -# 检查 Loki 是否正确接收日志 -kubectl port-forward -n monitoring svc/loki 3100:3100 & - -# 查询日志 -curl 'http://localhost:3100/loki/api/v1/query_range?query={job="kubernetes-pods"}&start=0&end=9999999999' - -# 应该返回最近的日志条目 - -# 检查特定应用的日志 -curl 'http://localhost:3100/loki/api/v1/query_range?query={app="user-rpc"}&start=0&end=9999999999' - -kill %1 -``` - -## 第九部分:ETCD 加密验证 - -如果已启用 ETCD 加密,执行以下验证: - -```bash -# 从 control plane 节点 -ssh - -# 检查 ETCD 配置 -sudo cat /etc/kubernetes/encryption-config.yaml | head -20 - -# 验证 kube-apiserver 正在使用加密配置 -sudo ps aux | grep kube-apiserver | grep encryption-provider - -# 创建新 Secret 进行测试 -kubectl create secret generic test-encryption -n juwan --from-literal=key=value - -# 检查 ETCD 中的数据是否加密 -# 注意:如果加密正确,数据应该不可读 -sudo ETCDCTL_API=3 etcdctl \ - --cert=/etc/kubernetes/pki/etcd/server.crt \ - --key=/etc/kubernetes/pki/etcd/server.key \ - --cacert=/etc/kubernetes/pki/etcd/ca.crt \ - --endpoints=127.0.0.1:2379 \ - get /registry/secrets/juwan/test-encryption - -# 输出应该是二进制数据,不可读(表示已加密) -# 或者使用十六进制 dump -sudo ETCDCTL_API=3 etcdctl \ - --cert=/etc/kubernetes/pki/etcd/server.crt \ - --key=/etc/kubernetes/pki/etcd/server.key \ - --cacert=/etc/kubernetes/pki/etcd/ca.crt \ - --endpoints=127.0.0.1:2379 \ - get /registry/secrets/juwan/test-encryption | od -A x -t x1z -v -``` - -## 第十部分:功能测试 - -### JWT 令牌生成和验证测试 - -如果已实现 JWT handlers,测试完整流程: - -```bash -# 1. 前向 user-api 服务 -kubectl port-forward -n juwan svc/user-api-svc 8888:8888 & - -# 2. 调用登录端点获取令牌 -TOKEN=$(curl -X POST http://localhost:8888/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"email":"user@example.com","password":"password"}' \ - | jq -r '.token') - -echo "Token: $TOKEN" - -# 3. 使用令牌访问受保护的端点 -curl -H "Authorization: Bearer $TOKEN" http://localhost:8888/api/v1/users/me - -# 4. 测试令牌刷新 -curl -X POST http://localhost:8888/api/v1/auth/refresh \ - -H "Content-Type: application/json" \ - -d "{\"token\":\"$TOKEN\"}" - -# 5. 测试无效令牌 -curl -H "Authorization: Bearer invalid-token" http://localhost:8888/api/v1/users/me -# 应该返回 401 Unauthorized - -kill %1 -``` - -### CSRF 保护测试 - -```bash -# 1. 前向 Envoy Gateway -kubectl port-forward -n juwan svc/envoy-gateway 8080:80 & - -# 2. 获取 CSRF 令牌(安全方法) -curl -i http://localhost:8080/ - -# 查看响应头中的 Set-Cookie,应该包含 csrf_token - -# 3. 提取 CSRF 令牌 -CSRF_TOKEN=$(curl -i http://localhost:8080/ 2>/dev/null | grep -i csrf_token | sed 's/.*csrf_token=//;s/;.*//') - -# 4. 使用 CSRF 令牌进行 POST 请求 -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -H "Cookie: csrf_token=$CSRF_TOKEN" \ - -H "X-CSRF-Token: $CSRF_TOKEN" \ - -d '{"email":"user@example.com","password":"password"}' - -# 5. 测试无效 CSRF 令牌(应该返回 403) -curl -X POST http://localhost:8080/api/v1/auth/login \ - -H "Cookie: csrf_token=valid_token" \ - -H "X-CSRF-Token: invalid_token" \ - -d '{"email":"user@example.com","password":"password"}' -# 应该返回 403 Forbidden - -kill %1 -``` - -## 第十一部分:故障排查 - -如果任何验证失败,运行以下诊断: - -### Pod 无法启动 - -```bash -# 显示 Pod 事件 -kubectl describe pod -n juwan - -# 查看完整日志(包括初始化容器) -kubectl logs -n juwan --all-containers=true --previous - -# 检查 Pod 资源限制是否导致 OOMKilled -kubectl get event -n juwan --sort-by='.lastTimestamp' -``` - -### 权限被拒绝错误 - -```bash -# 验证 ServiceAccount 是否正确 -kubectl get pod -n juwan -o jsonpath='{.spec.serviceAccountName}' - -# 检查 RBAC 绑定 -kubectl get rolebinding -n juwan -o wide - -# 手动测试权限 -kubectl auth can-i get secrets \ - --as=system:serviceaccount:juwan:user-rpc \ - -n juwan -``` - -### Redis 连接错误 - -```bash -# 检查 Redis Pods 状态 -kubectl get pods -n juwan -l redis=user-redis - -# 查看 Redis 日志 -kubectl logs -n juwan -l redis=user-redis - -# 测试 Redis 连接(从 user-rpc Pod) -kubectl exec -it -n juwan -- \ - redis-cli -h user-redis.juwan:6379 PING -# 应该返回 PONG -``` - -### ETCD 加密问题 - -```bash -# 验证加密配置 -kubectl get secret jwt-secret -n juwan -o json | jq '.data' - -# 如果 ETCD 加密启用,直接读取 ETCD 的数据应该是二进制的 -# 如果看到明文,说明加密未启用或配置不正确 -``` - -## 第十二部分:清理测试资源 - -```bash -# 删除测试 Secrets -kubectl delete secret test-encryption test-secret -n juwan --ignore-not-found - -# 清理前转发的端口 -lsof -i :9090 :3100 :8888 :8080 | grep LISTEN | awk '{print $2}' | xargs kill -9 -``` - -## 快速检查脚本 - -创建 `verify-jwt-setup.sh` 进行自动化验证: - -```bash -#!/bin/bash - -namespace="juwan" -echo "=== JWT Setup Verification ===" - -# 检查 Secret -echo -n "✓ JWT Secret存在: " -kubectl get secret jwt-secret -n $namespace &>/dev/null && echo "✓" || echo "✗" - -# 检查 ServiceAccounts -echo -n "✓ user-rpc ServiceAccount: " -kubectl get sa user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗" - -echo -n "✓ envoy-gateway ServiceAccount: " -kubectl get sa envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗" - -# 检查 RBAC -echo -n "✓ JWT RBAC Role: " -kubectl get role jwt-secret-reader -n $namespace &>/dev/null && echo "✓" || echo "✗" - -# 检查 Deployments -echo -n "✓ user-rpc Deployment: " -kubectl get deployment user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗" - -echo -n "✓ envoy-gateway Deployment: " -kubectl get deployment envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗" - -# 检查 Pods -echo -n "✓ user-rpc Pods运行中: " -[ $(kubectl get pods -n $namespace -l app=user-rpc --field-selector=status.phase=Running --no-headers | wc -l) -ge 1 ] && echo "✓" || echo "✗" - -echo -n "✓ envoy-gateway 运行中: " -kubectl get pods -n $namespace -l app=envoy-gateway --field-selector=status.phase=Running &>/dev/null && echo "✓" || echo "✗" - -echo "=== Verification Complete ===" -``` - -运行脚本: - -```bash -chmod +x verify-jwt-setup.sh -./verify-jwt-setup.sh -``` - -## 总结 - -所有检查项都通过后,JWT + ETCD 加密系统已准备就绪。下一步可以: - -1. 集成 JWT 验证到 RPC handlers -2. 实现令牌刷新端点 -3. 部署应用代码时启用 JWT 认证 -4. 监控令牌生成和验证指标 -5. 定期轮换加密密钥和 JWT 秘钥 +# 完整部署验证清单 + +完成所有部署后使用此清单验证系统是否正确配置和运行。 + +## 第一部分:基础设施验证 + +### Secret 和 RBAC 创建 + +```bash +# 检查 Secret 已创建 +kubectl get secret -n juwan | grep jwt-secret +# 预期输出: jwt-secret Created + +# 查看 Secret 详情(不显示敏感数据) +kubectl describe secret jwt-secret -n juwan +# 应该看到: +# Name: jwt-secret +# Namespace: juwan +# Type: Opaque +# Data +# ==== +# secret-key: + +# 验证 Secret 内容已正确加载 +kubectl get secret jwt-secret -n juwan -o jsonpath='{.data.secret-key}' | base64 -d | wc -c +# 预期输出: 应该是 32 个字符(32 字节密钥的 Base64 解码) +``` + +### ServiceAccount 验证 + +```bash +# 检查 user-rpc ServiceAccount +kubectl get sa user-rpc -n juwan +kubectl describe sa user-rpc -n juwan +# 应该显示正确的 Secrets 挂载 + +# 检查 envoy-gateway ServiceAccount +kubectl get sa envoy-gateway -n juwan +kubectl describe sa envoy-gateway -n juwan +``` + +### RBAC 权限验证 + +```bash +# 检查 Role 定义 +kubectl get role -n juwan -l app=jwt-secret-reader +kubectl describe role jwt-secret-reader -n juwan +# 应该显示: +# PolicyRule: +# Resources Non-Resource URLs Resource Names Verbs +# --------- ----------------- -------------- ----- +# secrets [] [jwt-secret] [get] + +# 检查 RoleBindings +kubectl get rolebinding -n juwan | grep jwt-secret-reader +# 应该显示两个绑定: jwt-secret-reader-user-rpc 和 jwt-secret-reader-envoy-gateway + +# 验证每个 RoleBinding +kubectl describe rolebinding jwt-secret-reader-user-rpc -n juwan +kubectl describe rolebinding jwt-secret-reader-envoy-gateway -n juwan +``` + +## 第二部分:权限测试 + +### 权限允许测试 + +```bash +# 测试 user-rpc 可以读 jwt-secret +kubectl auth can-i get secrets \ + --as=system:serviceaccount:juwan:user-rpc \ + --resource-name=jwt-secret \ + -n juwan +# 预期输出: yes + +# 测试 envoy-gateway 可以读 jwt-secret +kubectl auth can-i get secrets \ + --as=system:serviceaccount:juwan:envoy-gateway \ + --resource-name=jwt-secret \ + -n juwan +# 预期输出: yes + +# 测试 user-rpc 无法读其他 Secrets +kubectl auth can-i get secrets \ + --as=system:serviceaccount:juwan:user-rpc \ + -n juwan +# 预期输出: no + +# 测试其他 ServiceAccount 无法读 jwt-secret +kubectl auth can-i get secrets \ + --as=system:serviceaccount:juwan:default \ + --resource-name=jwt-secret \ + -n juwan +# 预期输出: no +``` + +## 第三部分:Deployment 配置验证 + +### user-rpc Deployment 验证 + +```bash +# 检查 ServiceAccountName 是否正确设置 +kubectl get deployment user-rpc -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}' +# 预期输出: user-rpc + +# 检查是否包含所有必需的环境变量 +kubectl get deployment user-rpc -n juwan -o yaml | grep -A 20 "env:" +# 应该包括: +# - name: JWT_SECRET_KEY +# valueFrom: +# secretKeyRef: +# name: jwt-secret +# key: secret-key + +# 检查 Pod 是否正在运行 +kubectl get pods -n juwan -l app=user-rpc +# 应该显示至少 3 个 Running 的 Pod + +# 验证 Pod 已加载 Secret(在 Pod 中执行) +kubectl exec -it $(kubectl get pod -n juwan -l app=user-rpc -o name | head -1) -n juwan -- env | grep -i jwt +# 应该输出环境变量,例如: +# JWT_SECRET_KEY=your-secret-jwt-key-change-this-in-production +``` + +### Envoy Gateway Deployment 验证 + +```bash +# 检查 ServiceAccountName 是否正确设置 +kubectl get deployment envoy-gateway -n juwan -o jsonpath='{.spec.template.spec.serviceAccountName}' +# 预期输出: envoy-gateway + +# 检查 Pod 是否正在运行 +kubectl get pods -n juwan -l app=envoy-gateway +# 应该显示 Running 的 Pod + +# 检查 Envoy 日志 +kubectl logs -n juwan -l app=envoy-gateway +# 应该看到启动日志,没有权限相关错误 +``` + +## 第四部分:Redis 连接验证 + +### Redis Cluster 验证 + +```bash +# 检查 RedisCluster CRD 状态 +kubectl get rediscluster -n juwan +# 应该显示 user-redis,Status 应该是 Healthy + +# 详细查看 RedisCluster 状态 +kubectl describe rediscluster user-redis -n juwan +# 应该显示: +# Status: +# Cluster Status: Healthy +# Nodes Ready: 3/3 +# Master: 1 +# Replicas: 2 + +# 检查 Redis Pods +kubectl get pods -n juwan | grep redis +# 应该显示 3 个 Redis Pod,都在 Running 状态 + +# 测试 Redis 连接 +kubectl run redis-cli --image=redis:latest --rm -it --restart=Never -- \ + redis-cli -h user-redis.juwan -c CLUSTER INFO +# 应该看到集群信息,cluster_state:ok 表示集群健康 +``` + +## 第五部分:应用启动日志检查 + +### user-rpc 启动日志 + +```bash +# 查看 user-rpc Pods 的启动日志 +kubectl logs -n juwan -l app=user-rpc --all-containers=true + +# 应该包含类似以下消息: +# - "Starting gRPC server on 0.0.0.0:9001" +# - "Redis Cluster connected successfully" 或 JWT Manager 初始化成功 +# - "Listening on metrics port 4001" + +# 如果有错误,查看详细日志 +kubectl logs -n juwan -l app=user-rpc -f --all-containers=true +``` + +### Envoy 启动日志 + +```bash +# 查看 Envoy 启动日志 +kubectl logs -n juwan -l app=envoy-gateway + +# 应该包含: +# - "[info] Configuration: /etc/envoy/envoy.yaml" +# - "[info] listener listening on 0.0.0.0:8080" +# - 没有权限相关错误 +``` + +## 第六部分:网络和服务发现验证 + +### Service 验证 + +```bash +# 检查 user-rpc-svc +kubectl get svc user-rpc-svc -n juwan +# 应该显示 ClusterIP 和两个端口 (9001/rpc 和 4001/metrics) + +# 检查 Envoy Gateway Service +kubectl get svc envoy-gateway -n juwan +# 应该显示 ClusterIP 和端口 80 + +# 检查 Redis Service +kubectl get svc -n juwan | grep redis +# 应该显示 user-redis(ClusterIP)服务 +``` + +### DNS 解析验证 + +```bash +# 测试服务名称解析 +kubectl run -it --rm debug --image=busybox --restart=Never -- \ + nslookup user-rpc-svc.juwan.svc.cluster.local +# 应该返回 ClusterIP 地址 + +kubectl run -it --rm debug --image=busybox --restart=Never -- \ + nslookup user-redis.juwan.svc.cluster.local +# 应该返回 ClusterIP 地址 +``` + +## 第七部分:监控和指标验证 + +### Prometheus 指标收集 + +```bash +# 检查 Prometheus 是否在收集指标 +kubectl port-forward -n monitoring svc/prometheus 9090:9090 & + +# 打开浏览器访问 http://localhost:9090 +# 查看 Status > Targets +# 应该看到 user-rpc-svc:4001 目标显示为 UP + +# 查询一个指标 +curl 'http://localhost:9090/api/v1/query?query=up{job="kubernetes-pods"}' +# 应该返回 user-rpc 的指标数据 + +# 关闭端口转发 +kill %1 +``` + +### 测试源代码级指标端点 + +```bash +# 从 user-rpc Pod 直接访问指标端点 +kubectl port-forward -n juwan svc/user-rpc-svc 4001:4001 & + +# 测试指标端点 +curl http://localhost:4001/metrics + +# 应该看到 Prometheus 格式的指标,例如: +# # HELP go_goroutines Number of goroutines that currently exist. +# # TYPE go_goroutines gauge +# go_goroutines 25 + +# 关闭端口转发 +kill %1 +``` + +## 第八部分:日志聚合验证(Loki) + +```bash +# 检查 Loki 是否正确接收日志 +kubectl port-forward -n monitoring svc/loki 3100:3100 & + +# 查询日志 +curl 'http://localhost:3100/loki/api/v1/query_range?query={job="kubernetes-pods"}&start=0&end=9999999999' + +# 应该返回最近的日志条目 + +# 检查特定应用的日志 +curl 'http://localhost:3100/loki/api/v1/query_range?query={app="user-rpc"}&start=0&end=9999999999' + +kill %1 +``` + +## 第九部分:ETCD 加密验证 + +如果已启用 ETCD 加密,执行以下验证: + +```bash +# 从 control plane 节点 +ssh + +# 检查 ETCD 配置 +sudo cat /etc/kubernetes/encryption-config.yaml | head -20 + +# 验证 kube-apiserver 正在使用加密配置 +sudo ps aux | grep kube-apiserver | grep encryption-provider + +# 创建新 Secret 进行测试 +kubectl create secret generic test-encryption -n juwan --from-literal=key=value + +# 检查 ETCD 中的数据是否加密 +# 注意:如果加密正确,数据应该不可读 +sudo ETCDCTL_API=3 etcdctl \ + --cert=/etc/kubernetes/pki/etcd/server.crt \ + --key=/etc/kubernetes/pki/etcd/server.key \ + --cacert=/etc/kubernetes/pki/etcd/ca.crt \ + --endpoints=127.0.0.1:2379 \ + get /registry/secrets/juwan/test-encryption + +# 输出应该是二进制数据,不可读(表示已加密) +# 或者使用十六进制 dump +sudo ETCDCTL_API=3 etcdctl \ + --cert=/etc/kubernetes/pki/etcd/server.crt \ + --key=/etc/kubernetes/pki/etcd/server.key \ + --cacert=/etc/kubernetes/pki/etcd/ca.crt \ + --endpoints=127.0.0.1:2379 \ + get /registry/secrets/juwan/test-encryption | od -A x -t x1z -v +``` + +## 第十部分:功能测试 + +### JWT 令牌生成和验证测试 + +如果已实现 JWT handlers,测试完整流程: + +```bash +# 1. 前向 user-api 服务 +kubectl port-forward -n juwan svc/user-api-svc 8888:8888 & + +# 2. 调用登录端点获取令牌 +TOKEN=$(curl -X POST http://localhost:8888/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"user@example.com","password":"password"}' \ + | jq -r '.token') + +echo "Token: $TOKEN" + +# 3. 使用令牌访问受保护的端点 +curl -H "Authorization: Bearer $TOKEN" http://localhost:8888/api/v1/users/me + +# 4. 测试令牌刷新 +curl -X POST http://localhost:8888/api/v1/auth/refresh \ + -H "Content-Type: application/json" \ + -d "{\"token\":\"$TOKEN\"}" + +# 5. 测试无效令牌 +curl -H "Authorization: Bearer invalid-token" http://localhost:8888/api/v1/users/me +# 应该返回 401 Unauthorized + +kill %1 +``` + +### CSRF 保护测试 + +```bash +# 1. 前向 Envoy Gateway +kubectl port-forward -n juwan svc/envoy-gateway 8080:80 & + +# 2. 获取 CSRF 令牌(安全方法) +curl -i http://localhost:8080/ + +# 查看响应头中的 Set-Cookie,应该包含 csrf_token + +# 3. 提取 CSRF 令牌 +CSRF_TOKEN=$(curl -i http://localhost:8080/ 2>/dev/null | grep -i csrf_token | sed 's/.*csrf_token=//;s/;.*//') + +# 4. 使用 CSRF 令牌进行 POST 请求 +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -H "Cookie: csrf_token=$CSRF_TOKEN" \ + -H "X-CSRF-Token: $CSRF_TOKEN" \ + -d '{"email":"user@example.com","password":"password"}' + +# 5. 测试无效 CSRF 令牌(应该返回 403) +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Cookie: csrf_token=valid_token" \ + -H "X-CSRF-Token: invalid_token" \ + -d '{"email":"user@example.com","password":"password"}' +# 应该返回 403 Forbidden + +kill %1 +``` + +## 第十一部分:故障排查 + +如果任何验证失败,运行以下诊断: + +### Pod 无法启动 + +```bash +# 显示 Pod 事件 +kubectl describe pod -n juwan + +# 查看完整日志(包括初始化容器) +kubectl logs -n juwan --all-containers=true --previous + +# 检查 Pod 资源限制是否导致 OOMKilled +kubectl get event -n juwan --sort-by='.lastTimestamp' +``` + +### 权限被拒绝错误 + +```bash +# 验证 ServiceAccount 是否正确 +kubectl get pod -n juwan -o jsonpath='{.spec.serviceAccountName}' + +# 检查 RBAC 绑定 +kubectl get rolebinding -n juwan -o wide + +# 手动测试权限 +kubectl auth can-i get secrets \ + --as=system:serviceaccount:juwan:user-rpc \ + -n juwan +``` + +### Redis 连接错误 + +```bash +# 检查 Redis Pods 状态 +kubectl get pods -n juwan -l redis=user-redis + +# 查看 Redis 日志 +kubectl logs -n juwan -l redis=user-redis + +# 测试 Redis 连接(从 user-rpc Pod) +kubectl exec -it -n juwan -- \ + redis-cli -h user-redis.juwan:6379 PING +# 应该返回 PONG +``` + +### ETCD 加密问题 + +```bash +# 验证加密配置 +kubectl get secret jwt-secret -n juwan -o json | jq '.data' + +# 如果 ETCD 加密启用,直接读取 ETCD 的数据应该是二进制的 +# 如果看到明文,说明加密未启用或配置不正确 +``` + +## 第十二部分:清理测试资源 + +```bash +# 删除测试 Secrets +kubectl delete secret test-encryption test-secret -n juwan --ignore-not-found + +# 清理前转发的端口 +lsof -i :9090 :3100 :8888 :8080 | grep LISTEN | awk '{print $2}' | xargs kill -9 +``` + +## 快速检查脚本 + +创建 `verify-jwt-setup.sh` 进行自动化验证: + +```bash +#!/bin/bash + +namespace="juwan" +echo "=== JWT Setup Verification ===" + +# 检查 Secret +echo -n "✓ JWT Secret存在: " +kubectl get secret jwt-secret -n $namespace &>/dev/null && echo "✓" || echo "✗" + +# 检查 ServiceAccounts +echo -n "✓ user-rpc ServiceAccount: " +kubectl get sa user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗" + +echo -n "✓ envoy-gateway ServiceAccount: " +kubectl get sa envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗" + +# 检查 RBAC +echo -n "✓ JWT RBAC Role: " +kubectl get role jwt-secret-reader -n $namespace &>/dev/null && echo "✓" || echo "✗" + +# 检查 Deployments +echo -n "✓ user-rpc Deployment: " +kubectl get deployment user-rpc -n $namespace &>/dev/null && echo "✓" || echo "✗" + +echo -n "✓ envoy-gateway Deployment: " +kubectl get deployment envoy-gateway -n $namespace &>/dev/null && echo "✓" || echo "✗" + +# 检查 Pods +echo -n "✓ user-rpc Pods运行中: " +[ $(kubectl get pods -n $namespace -l app=user-rpc --field-selector=status.phase=Running --no-headers | wc -l) -ge 1 ] && echo "✓" || echo "✗" + +echo -n "✓ envoy-gateway 运行中: " +kubectl get pods -n $namespace -l app=envoy-gateway --field-selector=status.phase=Running &>/dev/null && echo "✓" || echo "✗" + +echo "=== Verification Complete ===" +``` + +运行脚本: + +```bash +chmod +x verify-jwt-setup.sh +./verify-jwt-setup.sh +``` + +## 总结 + +所有检查项都通过后,JWT + ETCD 加密系统已准备就绪。下一步可以: + +1. 集成 JWT 验证到 RPC handlers +2. 实现令牌刷新端点 +3. 部署应用代码时启用 JWT 认证 +4. 监控令牌生成和验证指标 +5. 定期轮换加密密钥和 JWT 秘钥 diff --git a/docs/secrets/jwt-secret.yaml b/docs/secrets/jwt-secret.yaml index 65f02c2..3d34049 100644 --- a/docs/secrets/jwt-secret.yaml +++ b/docs/secrets/jwt-secret.yaml @@ -1,60 +1,60 @@ -apiVersion: v1 -kind: Secret -metadata: - name: jwt-secret - namespace: juwan -type: Opaque -data: - # base64 encoded: your-secret-jwt-key-change-this-in-production - secret-key: eW91ci1zZWNyZXQtand0LWtleS1jaGFuZ2UtdGhpcy1pbi1wcm9kdWN0aW9u ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: user-rpc - namespace: juwan ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: envoy-gateway - namespace: juwan ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: jwt-secret-reader - namespace: juwan -rules: - - apiGroups: [""] - resources: ["secrets"] - resourceNames: ["jwt-secret"] - verbs: ["get"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: user-rpc-jwt-secret-reader - namespace: juwan -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: jwt-secret-reader -subjects: - - kind: ServiceAccount - name: user-rpc - namespace: juwan ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: envoy-gateway-jwt-secret-reader - namespace: juwan -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: jwt-secret-reader -subjects: - - kind: ServiceAccount - name: envoy-gateway - namespace: juwan +apiVersion: v1 +kind: Secret +metadata: + name: jwt-secret + namespace: juwan +type: Opaque +data: + # base64 encoded: your-secret-jwt-key-change-this-in-production + secret-key: eW91ci1zZWNyZXQtand0LWtleS1jaGFuZ2UtdGhpcy1pbi1wcm9kdWN0aW9u +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: user-rpc + namespace: juwan +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: envoy-gateway + namespace: juwan +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: jwt-secret-reader + namespace: juwan +rules: + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["jwt-secret"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: user-rpc-jwt-secret-reader + namespace: juwan +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: jwt-secret-reader +subjects: + - kind: ServiceAccount + name: user-rpc + namespace: juwan +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: envoy-gateway-jwt-secret-reader + namespace: juwan +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: jwt-secret-reader +subjects: + - kind: ServiceAccount + name: envoy-gateway + namespace: juwan diff --git a/package-lock.json b/package-lock.json index 0ebc9e9..9121b31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1153 +1,1153 @@ -{ - "name": "st-1-example", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@inquirer/prompts": "^8.2.0", - "@inquirer/search": "^4.1.0", - "fuse.js": "^7.1.0", - "glob": "^13.0.1", - "hereby": "^1.11.1", - "postgres": "^3.4.8", - "prompts": "^2.4.2" - }, - "devDependencies": { - "execa": "^9.6.1" - } - }, - "node_modules/@inquirer/ansi": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.3.tgz", - "integrity": "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.0.4.tgz", - "integrity": "sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.3", - "@inquirer/core": "^11.1.1", - "@inquirer/figures": "^2.0.3", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/confirm": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.4.tgz", - "integrity": "sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.1", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.1.tgz", - "integrity": "sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.3", - "@inquirer/figures": "^2.0.3", - "@inquirer/type": "^4.0.3", - "cli-width": "^4.1.0", - "mute-stream": "^3.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^9.0.2" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/editor": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.4.tgz", - "integrity": "sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.1", - "@inquirer/external-editor": "^2.0.3", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.4.tgz", - "integrity": "sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.1", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.3.tgz", - "integrity": "sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==", - "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.2" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/figures": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.3.tgz", - "integrity": "sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - } - }, - "node_modules/@inquirer/input": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.4.tgz", - "integrity": "sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.1", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.4.tgz", - "integrity": "sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.1", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.4.tgz", - "integrity": "sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.3", - "@inquirer/core": "^11.1.1", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.2.0.tgz", - "integrity": "sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA==", - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^5.0.4", - "@inquirer/confirm": "^6.0.4", - "@inquirer/editor": "^5.0.4", - "@inquirer/expand": "^5.0.4", - "@inquirer/input": "^5.0.4", - "@inquirer/number": "^4.0.4", - "@inquirer/password": "^5.0.4", - "@inquirer/rawlist": "^5.2.0", - "@inquirer/search": "^4.1.0", - "@inquirer/select": "^5.0.4" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.0.tgz", - "integrity": "sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.1", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.0.tgz", - "integrity": "sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA==", - "license": "MIT", - "dependencies": { - "@inquirer/core": "^11.1.1", - "@inquirer/figures": "^2.0.3", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.0.4.tgz", - "integrity": "sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^2.0.3", - "@inquirer/core": "^11.1.1", - "@inquirer/figures": "^2.0.3", - "@inquirer/type": "^4.0.3" - }, - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.3.tgz", - "integrity": "sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==", - "license": "MIT", - "engines": { - "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/array-back": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", - "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", - "license": "MIT" - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/command-line-usage": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", - "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", - "license": "MIT", - "dependencies": { - "array-back": "^4.0.2", - "chalk": "^2.4.2", - "table-layout": "^1.0.2", - "typical": "^5.2.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/execa": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", - "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/merge-streams": "^4.0.0", - "cross-spawn": "^7.0.6", - "figures": "^6.1.0", - "get-stream": "^9.0.0", - "human-signals": "^8.0.1", - "is-plain-obj": "^4.1.0", - "is-stream": "^4.0.1", - "npm-run-path": "^6.0.0", - "pretty-ms": "^9.2.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^4.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": "^18.19.0 || >=20.5.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/execa/node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/figures": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", - "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fuse.js": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", - "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", - "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.1.2", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/hereby": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/hereby/-/hereby-1.11.1.tgz", - "integrity": "sha512-3tcp92aUN6mSmWslo/EIoz3AAKa9GPmiJ3g0ZgXC8NGZPyh4J3T+JoGfD4JTiL31SW+pFliKKHu1uxa7nwDv0g==", - "license": "MIT", - "dependencies": { - "command-line-usage": "^6.1.3", - "fastest-levenshtein": "^1.0.16", - "minimist": "^1.2.8", - "picocolors": "^1.1.0", - "pretty-ms": "^8.0.0" - }, - "bin": { - "hereby": "bin/hereby.js" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/human-signals": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", - "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mute-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", - "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm-run-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", - "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0", - "unicorn-magic": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-ms": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz", - "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/postgres": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.8.tgz", - "integrity": "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==", - "license": "Unlicense", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/porsager" - } - }, - "node_modules/pretty-ms": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", - "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==", - "license": "MIT", - "dependencies": { - "parse-ms": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/reduce-flatten": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", - "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-final-newline": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", - "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/table-layout": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", - "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", - "license": "MIT", - "dependencies": { - "array-back": "^4.0.1", - "deep-extend": "~0.6.0", - "typical": "^5.2.0", - "wordwrapjs": "^4.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/typical": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", - "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/unicorn-magic": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wordwrapjs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", - "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", - "license": "MIT", - "dependencies": { - "reduce-flatten": "^2.0.0", - "typical": "^5.2.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} +{ + "name": "st-1-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@inquirer/prompts": "^8.2.0", + "@inquirer/search": "^4.1.0", + "fuse.js": "^7.1.0", + "glob": "^13.0.1", + "hereby": "^1.11.1", + "postgres": "^3.4.8", + "prompts": "^2.4.2" + }, + "devDependencies": { + "execa": "^9.6.1" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.3.tgz", + "integrity": "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.0.4.tgz", + "integrity": "sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.1", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.4.tgz", + "integrity": "sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.1.tgz", + "integrity": "sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3", + "cli-width": "^4.1.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^9.0.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.0.4.tgz", + "integrity": "sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/external-editor": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.4.tgz", + "integrity": "sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-2.0.3.tgz", + "integrity": "sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.3.tgz", + "integrity": "sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.4.tgz", + "integrity": "sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.4.tgz", + "integrity": "sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.4.tgz", + "integrity": "sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.2.0.tgz", + "integrity": "sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.0.4", + "@inquirer/confirm": "^6.0.4", + "@inquirer/editor": "^5.0.4", + "@inquirer/expand": "^5.0.4", + "@inquirer/input": "^5.0.4", + "@inquirer/number": "^4.0.4", + "@inquirer/password": "^5.0.4", + "@inquirer/rawlist": "^5.2.0", + "@inquirer/search": "^4.1.0", + "@inquirer/select": "^5.0.4" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.0.tgz", + "integrity": "sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.0.tgz", + "integrity": "sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.1", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.0.4.tgz", + "integrity": "sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.3", + "@inquirer/core": "^11.1.1", + "@inquirer/figures": "^2.0.3", + "@inquirer/type": "^4.0.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.3.tgz", + "integrity": "sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "license": "MIT", + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/hereby": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/hereby/-/hereby-1.11.1.tgz", + "integrity": "sha512-3tcp92aUN6mSmWslo/EIoz3AAKa9GPmiJ3g0ZgXC8NGZPyh4J3T+JoGfD4JTiL31SW+pFliKKHu1uxa7nwDv0g==", + "license": "MIT", + "dependencies": { + "command-line-usage": "^6.1.3", + "fastest-levenshtein": "^1.0.16", + "minimist": "^1.2.8", + "picocolors": "^1.1.0", + "pretty-ms": "^8.0.0" + }, + "bin": { + "hereby": "bin/hereby.js" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz", + "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postgres": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.8.tgz", + "integrity": "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/pretty-ms": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", + "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==", + "license": "MIT", + "dependencies": { + "parse-ms": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "license": "MIT", + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "license": "MIT", + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index af724e4..82ad6c1 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ -{ - "type": "module", - "packageManager": "npm@11.9.0+sha512.04166853ddba142ca98f86fb57b1258a7c6c59ccb82acb3cf141b77a315898acaaed47395e74f7e0c7b69c486008e68be6a6381ef1aee5a23dd82e0e61decd68", - "dependencies": { - "@inquirer/prompts": "^8.2.0", - "@inquirer/search": "^4.1.0", - "fuse.js": "^7.1.0", - "glob": "^13.0.1", - "hereby": "^1.11.1", - "postgres": "^3.4.8", - "prompts": "^2.4.2" - }, - "devDependencies": { - "execa": "^9.6.1" - } -} +{ + "type": "module", + "packageManager": "npm@11.9.0+sha512.04166853ddba142ca98f86fb57b1258a7c6c59ccb82acb3cf141b77a315898acaaed47395e74f7e0c7b69c486008e68be6a6381ef1aee5a23dd82e0e61decd68", + "dependencies": { + "@inquirer/prompts": "^8.2.0", + "@inquirer/search": "^4.1.0", + "fuse.js": "^7.1.0", + "glob": "^13.0.1", + "hereby": "^1.11.1", + "postgres": "^3.4.8", + "prompts": "^2.4.2" + }, + "devDependencies": { + "execa": "^9.6.1" + } +}