killercoda CKA:Troubleshooting - 1

killercoda CKA:Troubleshooting - 1

D瓜哥
1. Troubleshooting - Pod Issue Troubleshooting - Pod Issue hello-kubernetes pod not running, fix that issue # @author D瓜哥 · https://www.diguage.com $ kubectl get pod NAME READY STATUS RESTARTS AGE hello-kubernetes 0/1 RunContainerError 2 (6s ago) 29s $ kubectl describe pod hello-kubernetes Name: hello-kubernetes Namespace: default Priority: 0 Service Account: default Node: node01/172.30.2.2 Start Time: Mon, 20 Jan 2025 07:21:57 +0000 Labels: <none> Annotations: cni.projectcalico.org/containerID: 2e010161283b56bfd70d604c31ece3dc3189882f1e24c2ea57647dbaec3b2bdb cni.projectcalico.org/podIP: 192.168.1.4/32 cni.projectcalico.org/podIPs: 192.168.1.4/32 Status: Running IP: 192.168.1.4 IPs: IP: 192.168.1.4 Containers: echo-container: Container ID: containerd://4f01851fcb908cd7bd1031a1726b8b75873d69fb246a5eebdd5c3dc003be7c19 Image: redis Image ID: docker.io/library/redis@sha256:ca65ea36ae16e709b0f1c7534bc7e5b5ac2e5bb3c97236e4fec00e3625eb678d Port: <none> Host Port: <none> Command: shell -c while true; do echo 'Hello Kubernetes'; sleep 5; done State: Waiting Reason: CrashLoopBackOff Last State: Terminated Reason: StartError Message: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "shell": executable file not found in $PATH: unknown Exit Code: 128 Started: Thu, 01 Jan 1970 00:00:00 +0000 Finished: Mon, 20 Jan 2025 07:22:20 +0000 Ready: False Restart Count: 2 Environment: <none> Mounts: /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-xk5qj (ro) Conditions: Type Status PodReadyToStartContainers True Initialized True Ready False ContainersReady False PodScheduled True Volumes: kube-api-access-xk5qj: Type: Projected (a volume that contains injected data from multiple sources) TokenExpirationSeconds: 3607 ConfigMapName: kube-root-ca.crt ConfigMapOptional: <nil> DownwardAPI: true QoS Class: BestEffort Node-Selectors: <none> Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s node.kubernetes.io/unreachable:NoExecute op=Exists for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 41s default-scheduler Successfully assigned default/hello-kubernetes to node01 Normal Pulled 35s kubelet Successfully pulled image "redis" in 5.57s (5.57s including waiting). Image size: 45006722 bytes. Normal Pulled 33s kubelet Successfully pulled image "redis" in 422ms (422ms including waiting). Image size: 45006722 bytes. Normal Pulling 19s (x3 over 40s) kubelet Pulling image "redis" Normal Created 18s (x3 over 35s) kubelet Created container echo-container Warning Failed 18s (x3 over 34s) kubelet Error: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "shell": executable file not found in $PATH: unknown Normal Pulled 18s kubelet Successfully pulled image "redis" in 467ms (467ms including waiting). Image size: 45006722 bytes. Warning BackOff 6s (x4 over 32s) kubelet Back-off restarting failed container echo-container in pod hello-kubernetes_default(5a459cd4-866a-4e57-8d44-ae83156e1e0b) $ kubectl get pod hello-kubernetes -o yaml | tee pod.yaml apiVersion: v1 kind: Pod metadata: annotations: cni.projectcalico.org/containerID: 2e010161283b56bfd70d604c31ece3dc3189882f1e24c2ea57647dbaec3b2bdb cni.projectcalico.org/podIP: 192.168.1.4/32 cni.projectcalico.org/podIPs: 192.168.1.4/32 kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"hello-kubernetes","namespace":"default"},"spec":{"containers":[{"command":["shell","-c","while true; do echo 'Hello Kubernetes'; sleep 5; done"],"image":"redis","name":"echo-container"}]}} creationTimestamp: "2025-01-20T07:21:57Z" name: hello-kubernetes namespace: default resourceVersion: "2157" uid: 5a459cd4-866a-4e57-8d44-ae83156e1e0b spec: containers: - command: - shell - -c - while true; do echo 'Hello Kubernetes'; sleep 5; done image: redis imagePullPolicy: Always name: echo-container resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: kube-api-access-xk5qj readOnly: true dnsPolicy: ClusterFirst enableServiceLinks: true nodeName: node01 preemptionPolicy: PreemptLowerPriority priority: 0 restartPolicy: Always schedulerName: default-scheduler securityContext: {} serviceAccount: default serviceAccountName: default terminationGracePeriodSeconds: 30 tolerations: - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 volumes: - name: kube-api-access-xk5qj projected: defaultMode: 420 sources: - serviceAccountToken: expirationSeconds: 3607 path: token - configMap: items: - key: ca.crt path: ca.crt name: kube-root-ca.crt - downwardAPI: items: - fieldRef: apiVersion: v1 fieldPath: metadata.namespace path: namespace # 省略了 status 字段 $ vim pod.yaml # 根据提示,没有 shell,将 shell 修改为 sh 即可。 $ kubectl replace -f pod.yaml Error from server (Conflict): error when replacing "pod.yaml": Operation cannot be fulfilled on pods "hello-kubernetes": the object has been modified; please apply your changes to the latest version and try again # 不能替换,就直接删除,再重建 $ kubectl delete -f pod.yaml --force --grace-period 0 Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely. pod "hello-kubernetes" force deleted $ kubectl apply -f pod.yaml pod/hello-kubernetes created $ kubectl get pod NAME READY STATUS RESTARTS AGE hello-kubernetes 1/1 Running 0 5s
killercoda CKA:Workloads & Scheduling

killercoda CKA:Workloads & Scheduling

D瓜哥
1. Workloads & Scheduling - Pod Workloads & Scheduling - Pod Fresher deployed a pod named my-pod. However, while specifying the resource limits, they mistakenly given 100Mi storage limit instead of 50Mi node doesn’t have sufficient resources, So change it to 50Mi only. # @author D瓜哥 · https://www.diguage.com $ kubectl get pod my-pod -o yaml | tee pod.yaml apiVersion: v1 kind: Pod metadata: annotations: cni.projectcalico.org/containerID: 8414bfefda21fa6ca74ef8d499c92a22ae6cc0dbb6d0bc4d82eb0129a795d75d cni.projectcalico.org/podIP: 192.168.1.4/32 cni.projectcalico.org/podIPs: 192.168.1.4/32 kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"my-pod","namespace":"default"},"spec":{"containers":[{"image":"nginx:latest","name":"my-container","resources":{"limits":{"memory":"100Mi"},"requests":{"memory":"50Mi"}}}]}} creationTimestamp: "2025-01-14T07:53:50Z" name: my-pod namespace: default resourceVersion: "2026" uid: fcf1e97e-cec0-45b0-b82d-766ad0c51823 spec: containers: - image: nginx:latest imagePullPolicy: Always name: my-container resources: limits: memory: 100Mi requests: memory: 50Mi terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: kube-api-access-thchj readOnly: true dnsPolicy: ClusterFirst enableServiceLinks: true nodeName: node01 preemptionPolicy: PreemptLowerPriority priority: 0 restartPolicy: Always schedulerName: default-scheduler securityContext: {} serviceAccount: default serviceAccountName: default terminationGracePeriodSeconds: 30 tolerations: - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 volumes: - name: kube-api-access-thchj projected: defaultMode: 420 sources: - serviceAccountToken: expirationSeconds: 3607 path: token - configMap: items: - key: ca.crt path: ca.crt name: kube-root-ca.crt - downwardAPI: items: - fieldRef: apiVersion: v1 fieldPath: metadata.namespace path: namespace # 省略没用的 status 字段 $ vim pod.yaml # 将 limit 中,100Mi 改为 50Mi $ kubectl delete -f pod.yaml --force --grace-period 0 Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely. pod "my-pod" force deleted $ kubectl apply -f pod.yaml pod/my-pod created
killercoda CKA:Storage

killercoda CKA:Storage

D瓜哥
1. Storage - Persistent Volume Storage - Persistent Volume Create a PersistentVolume (PV) named black-pv-cka with the following specifications: Volume Type: hostPath Path: /opt/black-pv-cka Capacity: 50Mi # @author D瓜哥 · https://www.diguage.com $ vim pv.yaml # 编写 YAML 文件 $ cat pv.yaml apiVersion: v1 kind: PersistentVolume metadata: name: black-pv-cka spec: capacity: storage: 50Mi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain hostPath: path: /opt/black-pv-cka $ kubectl apply -f pv.yaml persistentvolume/black-pv-cka created 2. Storage - Persistent Volume Claim Storage - Persistent Volume Claim
killercoda CKA:Services & Networking

killercoda CKA:Services & Networking

D瓜哥
1. Services & Networking - Services Services & Networking - Services You have an existing Nginx pod named nginx-pod. Perform the following steps: Expose the nginx-pod internally within the cluster using a Service named nginx-service . Use port forwarding to service to access the Welcome content of nginx-pod using the curl command. # @author D瓜哥 · https://www.diguage.com $ kubectl get pod --show-labels NAME READY STATUS RESTARTS AGE LABELS nginx-pod 1/1 Running 0 8m48s app=nginx $ cat svc.yaml apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: nginx ports: - name: http protocol: TCP port: 80 targetPort: 80 $ kubectl apply -f svc.yaml service/nginx-service created $ kubectl port-forward service/nginx-service 8081:80 Forwarding from 127.0.0.1:8081 -> 80 Forwarding from [::1]:8081 -> 80 Handling connection for 8081 # 打开另外一个终端 $ curl localhost:8081 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
killercoda CKA:Architecture, Installation & Maintenance

killercoda CKA:Architecture, Installation & Maintenance

D瓜哥
1. Architecture, Installation & Maintenance - Create Pod Architecture, Installation & Maintenance - Create Pod Create a pod called sleep-pod using the nginx image and also sleep (using command ) for give any value for seconds. # @author D瓜哥 · https://www.diguage.com $ cat nginx.yaml apiVersion: v1 kind: Pod metadata: name: sleep-pod spec: containers: - name: nginx image: nginx command: - sleep - "3600" $ kubectl apply -f nginx.yaml pod/sleep-pod created $ kubectl get pod NAME READY STATUS RESTARTS AGE sleep-pod 1/1 Running 0 5s
理解数据库分片

理解数据库分片

D瓜哥
最近在 DigitalOcean 社区看到一篇文章,讲解数据库分片架构的,感觉非常不错,图文并茂,翻译过来,分享给需要的朋友。 介绍 任何应用程序或网站,如果出现大幅增长,最终都需要进行扩展,以适应流量的增加。对于数据驱动型应用程序和网站来说,在进行扩展时必须确保数据的安全性和完整性。很难预测一个网站或应用程序会变得多受欢迎,或者它的受欢迎程度会维持多久,这就是为什么一些组织会选择一种允许他们动态扩展数据库的数据库架构。 在这篇概念性文章中,我们将讨论这样一种数据库架构:分片数据库。近年来,分片数据库受到了广泛关注,但很多人并不清楚什么是分片数据库,也不知道在哪些情况下分片数据库才有意义。我们将介绍什么是分片、分片的一些主要优点和缺点,以及几种常见的分片方法。 什么是分片? 分片是一种与水平分区相关的数据库架构模式,即把一个表的行分成多个不同的表,称为分区。每个分区都有相同的模式和列,但也有完全不同的行。同样,每个分区中的数据都是唯一的,与其他分区中的数据无关。 从水平分区与垂直分区的关系角度来思考水平分区可能会有所帮助。在垂直分区表中,整个列都被分离出来并放入新的、不同的表中。一个垂直分区中的数据独立于所有其他分区中的数据,每个分区都有不同的行和列。下图说明了如何对表格进行水平和垂直分区: 图 1. 水平分区与垂直分区 分片是指将数据分割成两个或多个较小的块,称为逻辑分片。然后,逻辑分片分布在不同的数据库节点上,称为物理分片,物理分片可容纳多个逻辑分片。尽管如此,所有分片中保存的数据共同代表了一个完整的逻辑数据集。 数据库分片是无共享架构的典范。这意味着分片是独立的,它们不共享任何相同的数据或计算资源。不过,在某些情况下,将某些表复制到每个分片中作为参考表是有意义的。例如,假设有一个应用程序的数据库依赖于重量测量的固定转换率。通过将包含必要转换率数据的表复制到每个分片中,有助于确保每个分片中都包含查询所需的所有数据。 通常,分片是在应用程序级实现的,这意味着应用程序包含定义向哪个分片传输读写的代码。不过,有些数据库管理系统内置了分片功能,允许你直接在数据库级实施分片。 鉴于以上对分片的概述,让我们来看看这种数据库架构的一些优点和缺点。 分片的优点 对数据库进行分片的主要吸引力在于,它有助于促进水平扩展,也称为向外扩展,横向扩展。水平扩展是指在现有堆栈中添加更多机器,以分散负载,允许更多流量和更快处理。这通常与垂直扩展(也称向上扩展)形成对比,后者涉及升级现有服务器的硬件,通常是增加更多内存或 CPU。 在一台机器上运行一个关系数据库,并根据需要通过升级其计算资源来扩大其规模相对简单。但归根结底,任何非分布式数据库在存储和计算能力方面都是有限的,因此可以自由横向扩展,会让你的设置更加灵活。 一些人选择分片数据库架构的另一个原因是为了加快查询响应速度。在未分片的数据库上提交查询时,数据库可能需要搜索查询表中的每一行,然后才能找到所需的结果集。对于使用大型单体数据库的应用程序来说,查询速度会慢得令人望而却步。不过,通过将一个表分片成多个表后,查询需要处理的行数就会减少,返回结果集的速度也会快得多。 分片还可以减轻中断造成的影响,从而提高应用程序的可靠性。如果您的应用程序或网站依赖的是未分片的数据库,中断有可能导致整个应用程序不可用。 而使用分片数据库时,故障可能只影响单个分片。尽管这可能会导致部分用户无法使用应用程序或网站的某些部分,但总体影响仍小于整个数据库崩溃的影响。 分片的缺点 虽然分片可以使数据库的扩展更容易并提高性能,但它也会带来一些限制。在此,我们将讨论其中的一些限制,以及为什么要避免使用分片。 人们在使用分片时遇到的第一个困难是正确实施分片数据库架构的复杂性。如果操作不当,分片过程很有可能导致数据丢失或表损坏。即使操作正确,分片也可能对团队的工作流程产生重大影响。用户必须跨多个分片位置管理数据,而不是从一个入口点访问和管理数据,这可能会对某些团队造成干扰。 用户在对数据库进行分片后有时会遇到一个问题,那就是分片最终会变得不平衡。举例来说,假设你的数据库有两个独立的分片,一个用于存储姓氏以字母 A 至 M 开头的客户,另一个用于存储姓氏以字母 N 至 Z 开头的客户。然而,你的应用程序为大量姓氏以字母 G 开头的人提供服务。 A-M 分区已成为所谓的数据库热点。在这种情况下,分片给数据库带来的任何好处都会被速度变慢和崩溃所抵消。数据库很可能需要修复和重新分片,以使数据分布更均匀。 另一个主要缺点是,一旦数据库被分片,就很难将其恢复到未分片的架构。数据库分片前的任何备份都不包括分片后写入的数据。 因此,要重建未分片的原始架构,就需要将新的分片数据与旧的备份合并,或者将分片后的数据库变回单一数据库,这两种方法都会耗费大量成本和时间。 最后一个需要考虑的缺点是,并非每个数据库引擎都支持分片。例如,PostgreSQL 不包括自动分片功能,但可以手动分片 PostgreSQL 数据库。 有一些 Postgres 变种确实包含自动分片功能,但它们往往落后于最新的 PostgreSQL 版本,而且缺乏某些其他功能。一些专门的数据库技术(如 MySQL Cluster 或某些数据库即服务产品(如 MongoDB Atlas))确实包含自动分片功能,但这些数据库管理系统的普通版本并不包含。因此,分片通常需要“自己开发”。这意味着通常很难找到分片文档或故障排除技巧。 当然,这些只是分片前需要考虑的一些一般性问题。根据其用例,对数据库进行分片可能会有更多潜在的缺点。 现在,我们已经介绍了分片的一些缺点和优点,下面将介绍几种不同的分片数据库架构。 分片架构 一旦决定对数据库进行分片,接下来需要考虑的就是如何分片。在运行查询或将输入数据分发到分片表或数据库时,将数据分发到正确的分片至关重要。否则,可能会导致数据丢失或查询缓慢。在本节中,我们将介绍几种常见的分片架构,每种架构都使用略有不同的流程在分片间分发数据。 基于键的分片 基于密钥的分片,也称为基于散列的分片,涉及使用从新写入的数据中提取的值,例如客户的 ID 编号、客户端应用程序的 IP 地址、邮政编码等并将其输入散列函数,以确定数据应进入哪个分片。散列函数是一种输入数据(如客户电子邮件)并输出离散值(即散列值)的函数。在分片的情况下,散列值是一个分片 ID,用于确定输入的数据将存储在哪个分片上。整个过程如下: 图 2. 基于键的分片 为确保条目以一致的方式放置于正确的分片,输入散列函数的值都应来自同一列。此列被称为分片键。简单来说,分片键与主键类似,都是用于为单个行建立唯一标识符的列。从广义上讲,分片键应该是静态的,也就是说,它不应该包含可能会随时间变化的值。否则,会增加更新操作的工作量,并可能降低性能。
基于 Docker 搭建开发环境(三):链路追踪

基于 Docker 搭建开发环境(三):链路追踪

D瓜哥
基于 Docker 搭建开发环境系列: 基于 Docker 搭建开发环境(一):数据库+监控 基于 Docker 搭建开发环境(二):EFK 日志套件 基于 Docker 搭建开发环境(三):链路追踪 在上一篇文章 基于 Docker 搭建开发环境(一):数据库+监控 和 基于 Docker 搭建开发环境(二):EFK 日志套件 两篇文章中,分别介绍了“数据库+监控”和“EFK 日志套件”。这篇文章给大家分享一下如何在本地搭建起一套简单的分布式链路追踪。 在 AI 的帮助下,如同砍瓜切菜一样,非常迅速地就完成了 基于 Docker 搭建开发环境(二):EFK 日志套件 的搭建。原以为搞这个也会分分钟的问题,结果应用的追踪数据一致无法正常发送到 Jaeger 中,各种改端口号都不行。后来,无意间看了 OpenTelemetry 的配置文档,增加了一个协议配置,全部流程竟然通了,非常神奇! 站在更高的视角去看,链路追踪其实是可观测性的一部分,包括上篇文章的日志,也是可观测性的一部分。日志、追踪、度量,三者是相辅相成的。 图 1. 可观测性 在 OpenTelemetry 出现之前,日志、追踪、度量是分离的,三者各各自为战。而 OpenTelemetry 的出现,则是试图将三者统一。目前 OpenTelemetry 是云原生架构中,最炙手可热的分布式链路追踪解决方案,它提供了一套相关标准,各个厂商可以在这套标准之上进行各种各样的组件开发,大家可以根据自己的需要,选择不同的组件,进行可插拔式的安装。 图 2. OpenTelemetry 的野心 在这篇文章中,链路追踪的解决方案选择的是 OpenTelemetry + OpenTelemetry Collector + Jaeger。 OpenTelemetry OpenTelemetry 并不需要在 Docker 中启动或者配置什么。在目前的架构中,Jaeger 是作为 OpenTelemetry 的一个实现来出现的。 OpenTelemetry 需要做的就是下载一个 Java Agent,执行 docker/config/opentelemetry/download-opentelemetry-agent.sh 脚本即可下载最新版的 Java Agent。在业务应用启动时,增加如下 JVM 参数:
基于 Docker 搭建开发环境(二):EFK 日志套件

基于 Docker 搭建开发环境(二):EFK 日志套件

D瓜哥
基于 Docker 搭建开发环境系列: 基于 Docker 搭建开发环境(一):数据库+监控 基于 Docker 搭建开发环境(二):EFK 日志套件 基于 Docker 搭建开发环境(三):链路追踪 在上一篇文章 基于 Docker 搭建开发环境(一):数据库+监控 中,介绍了一下如何使用 Docker 搭建起 MySQL + NACOS + Prometheus + Grafana 集成数据库、注册中心+配置管理、监控的开发环境。这篇文章来介绍一下如何在原来的基础上接入 Elasticsearch + Fluentd + Kibana 套件,并且将 NACOS 的日志接入到 Elasticsearch 里。 Elasticsearch 由于 Elasticsearch 8+ 的版本修改了安全策略,不允许 Kibana 使用超级管理员 elastic 连接 Elasticsearch,这里选用 7.x 版本做演示。 还有一点需要提醒,在设置 Elasticsearch 的超级管理员 elastic 的账户密码时,如果密码是全部的阿拉伯数字,那么需要用双引号或者单引号括起来。 在测试中,还遇到一个磁盘过载导致的只读问题。解决方式如下: curl -X GET "localhost:9200/_cat/allocation?v&pretty" 查看磁盘使用情况 解除只读状态 $ curl -X PUT "localhost:9200/test/_settings" -H 'Content-Type: application/json' -d' { "index.blocks.read_only_allow_delete": null } '
基于 Docker 搭建开发环境(一):数据库+监控

基于 Docker 搭建开发环境(一):数据库+监控

D瓜哥
基于 Docker 搭建开发环境系列: 基于 Docker 搭建开发环境(一):数据库+监控 基于 Docker 搭建开发环境(二):EFK 日志套件 基于 Docker 搭建开发环境(三):链路追踪 去年,很多同事要换 Mac 本,所以,写了 新 Mac 安装软件脚本,方便大家一键换机。最近想玩一下 Spring Cloud 以及相关周边的部署、监控等开源解决方案。由于组件众多及为了便于迁移和共享,计划基于 Docker 及 Docker Compose 搭建一套相关的开发环境。记录一下,方便有相同需求的朋友借鉴。 最新版的 Docker 在下载镜像时,会先访问一下 Docker 的官方站点。由于国内众所周知的网络情况,访问 Docker 官方站点总失败。所以,即使配置了国内 Docker 镜像站点也会失败。只需要将 Docker 软件回滚到 4.30.0 即可。(Mac 下验证有效,其他操作系统待进一步验证。) MySQL 开发中,最常用的应该就是数据库了。所以,先来搞 MySQL 数据库。 创建如下目录结构,并添加相关相关文件: $ tree . ├── README.adoc ├── clean.sh ├── data │ └── mysql │ └── .gitkeep ├── docker │ ├── config │ │ └── mysql │ │ └── init.sql │ ├── env │ │ └── mysql.env │ └── images │ └── mysql.dockerfile └── docker-compose.yml
再谈 DDD 是银弹吗?

再谈 DDD 是银弹吗?

D瓜哥
在 DDD 是银弹吗? 中,D瓜哥分享了关于领域驱动设计的三个问题。最近在读一本书 《架构设计2.0:大型分布式系统架构方法论与实践》。(这本书还不错,推荐)这本书中,花了两个章节的篇幅,重点谈论了领域驱动设计。引用书中的观点,结合个人开发经验,再来谈一谈 DDD 是否是银弹? 软件建模的困难 首先,必须面对的一个事实是:软件建模,困难重重;尤其是对于复杂业务的建模,更是难上加难。 对于复杂业务的软件开发,其生命周期大概分为如下五个阶段: 确定业务目标和业务价值。 比如某消费信贷业务。 目标被拆解成一系列核心功能点。 比如消费信贷下的授信、交易、账务等。 围绕这些功能点定义业务流程、业务规则,以及整个过程设计什么样的业务数据或业务对象。 比如账单分期金额必须大于 100 元。 领域建模。 比如对账务系统进行建模。 基于领域模型做技术架构的设计。 比如是否要做读写分离?是否要做分库分表等? 软件建模的本质是找出现实世界中的“不变形”。但是,现实世界中,唯一不变的就是这个世界在不断变化!所以,建模的过程也是一个反复的过程。如下图: 图 1. 复杂业务软件开发的生命周期 几乎不存在稳定的领域模型 我们追求一个稳定的领域模型,但是,现实却给了我们重重一击:稳定的模型几乎不可能做到。原因如下: 意识问题。 在消费、业务及产品等关注的是业务流程。唯独开发人员要将业务流程转化成业务模型。 现实世界的复杂性。 现实业务是复杂的,建模只是抽取了一个现实业务某一时刻的业务形态。但是,业务形态会有变化的,比如取现前期不可分期,后期业务迭代可能就会运行进行分期。 迭代速度。 互联网公司要求“小步快跑,快速迭代”。这与模型的稳定其实是矛盾的。为了业务的迭代速度,只能牺牲模型的稳定性,为了赶工期,只能在模型上不断打补丁。 火候的掌握。 开发人员的设计能力无法一蹴而就。既需要思考,又需要反复练习。在快速的业务迭代和人员流动下,开发人员根本没有时间锤炼自己的设计能力。那么,对于设计火候的掌握,也就很难达到理想中的水平。 领域驱动设计的困难 书中总结了实施领域驱动设计的五个困难,D瓜哥逐一谈谈自己的看法: 领域驱动设计本身只是一套思维方法,而不是要严格执行的标准,所以其本身弹性很大。 这个问题,D瓜哥在上一篇文章中已经讨论过了。弹性太大,就有太多值得商榷的地方,也许初次开发,还可以按照某个人的想法一以贯之。但是,随着参与维护的人员增多,每个人都会不由自主地会带入个人的一些想法,各种想法的碰撞,必然就会引入代码结构的混乱。 思维方式的转换很难。 绝大多数面向业务的开发人员,尤其是 Java 开发人员,对三层架构已经有根深蒂固的认识。思维方式已经被打上深深的烙印,想要改变,坦白讲,极其困难。尤其是,没有一个统一的标准和广泛认可的实现范例,完全靠摸着石头过河,必然会“一千个读者,就有一千个哈姆雷特。” 领域驱动设计的实施需要强大的技术基础实施来保证。 D瓜哥私以为这个倒不是什么问题。针对技术问题,尤其是一些共性问题,都有成熟的解决方案。只要能合理搭积木,就可以解决相应的问题。 大量存量的老系统,重构成本大于收益,没有重构动力。 编程第一准则:代码能跑就不要动。重构引入的问题谁来解决?重构带来的事故谁来负责?这个时候必须祭出这张图了: 图 2. 代码能跑就不要动 当然,私以为不是程序员反感重构代码,更多是因为下面这个因素。 在互联网的快速开发迭代面前,很少有人可以静下心来在软件方法论层面去精雕细琢,更多的是快速堆砌功能,完成业务需求开发。 业务的快速迭代,导致根本没有时间让开发人员去优化代码。可口的饭菜需要恰当的火候和足够的时间,优雅的软件建模也需要恰当的火候和足够的时间。精心地软件建模需要三个月,业务让你一个月上线,而且还是加班加点才能干完。结合实际来看,绝大多数情况都会想业务妥协吧?! 领域驱动设计的出路 书中的观点是做个折中:在宏观层面,遵循领域驱动设计的方法论;在微观层面,不严格遵循领域驱动设计的方法论。 D瓜哥是这样理解的:可以利用领域驱动设计里面的限界上下文的思想,把领域做个分割,划分成业务更聚合的子域。在子域内部,提炼出统一语言,来规范业务、产品和开发沟通的业务术语。在子域交互的接口层面,进行精心设计,精雕细琢。至于子域及接口的内部实现,就交给开发团队自己决策,只要满足对应的技术指标(比如每秒要支撑多大的访问量)即可。 在部门内部讨论时,D瓜哥还给出了一个更具操作性和落地性的方案:现实面临的问题是代码冗余,技术欠债,不容易维护。先放下关于领域驱动设计的无谓讨论,利用每一次开发的机会,把冗余代码删除,把代码重构和优化,一步一步地精炼代码,即使不谈领域驱动设计,相信在逐步重构和优化下,技术欠债会逐渐弥补,可维护性也会逐步提高。