首页 文章 精选 留言 我的

精选列表

搜索[最佳实践],共10001篇文章
优秀的个人博客,低调大师

最佳实践】利用OpenNJet实现灰度发布

在应用的新版本测试发布过程中,经常需要先使用部分选定的账号进行验证,待验证完成后,再逐步将业务流量切换到新版本,直至所有流量均切换到新的集群。 测试账号的检测可以使用多种方式,如通过Header中字段,Cookie中的值,或者URL 中的参数等方式。 在之后章节的配置及场景中,将使用HTTP Header中的staffid 字段做为测试账号的判断依据,7xxxx 的账号将被认定为测试工号。 初始静态配置 初始配置中,将设置两组upstream: backendA , backendB , 并通过split_clients 指令设置使用header中的staffid值做为流量配比依据,并设置100%的流量将指向后端 backendA。 split_clients $http_staffid $backend { 100% backendA; * backendB; } 完整配置如下: njet.conf worker_processes auto; cluster_name app1; node_name node1; error_log logs/error.log info; load_module modules/njt_http_split_clients_2_module.so; load_module modules/njt_http_dyn_bwlist_module.so; load_module modules/njt_http_lua_module.so; load_module modules/njt_dyn_ssl_module.so; load_module modules/njt_agent_dynlog_module.so; load_module modules/njt_http_location_module.so; helper ctrl modules/njt_helper_ctrl_module.so njet_ctrl.conf; helper broker modules/njt_helper_broker_module.so ; events { worker_connections 1024; } http { split_clients $http_staffid $backend { 100% backendA; * backendB; } upstream backendA { server 192.168.1.4:18081; } upstream backendB { server 192.168.2.4:18081; } server { listen 18080 ssl; ssl_certificate cert/server.crt; ssl_certificate_key cert/server.key; server_name www.test.com; location / { proxy_pass http://${backend}; } } } njet_ctrl.conf load_module modules/njt_http_sendmsg_module.so; load_module modules/njt_ctrl_config_api_module.so; load_module modules/njt_helper_health_check_module.so; load_module modules/njt_http_upstream_api_module.so; load_module modules/njt_http_location_api_module.so; load_module modules/njt_doc_module.so; load_module modules/njt_http_vtsd_module.so; error_log logs/error_ctrl.log error; events { worker_connections 1024; } http { include mime.types; access_log off; server { listen 8081; location / { return 200 "njet control panel\n"; } location /hc { health_check_api; } location /api { api write=on; } location /kv { dyn_sendmsg_kv; } location /config { config_api; } location /doc { doc_api; } location /dyn_loc { dyn_location_api; } location /metrics { vhost_traffic_status_display; vhost_traffic_status_display_format html; } } } 灰度发布步骤 ​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​动态添加location 当新版本已经部署到backendB 集群后, 通过动态location提供的API 添加一个条件location, location 中设置所有的 7xxxx 测试工号将访问到 backendB。Njet 提供的条件表达式中,可以使用$http_* 获取到请求header 中的值, $http_staffid ~* "7.*" 表达式将匹配所有header中staffid 为7xxxx 的请求。 使用测试工号测试 通过上一个步骤添加条件location 后, 所有非 7xxxx 的工号将继续访问到集群 backendA, 所有 7xxxx 做为测试工号,将访问到集群backendB。 ​​​​​​​删除表达式location 当测试完成后,可以使用动态API 删除条件location。 删除条件location后,所有的流量均指向backendA, 包括 7xxxx 的测试工号。 ​​​​​​​逐步调整分流比例直至100% 使用模块提供的动态配置API, 逐步调整分流比例。 如调整到 50:50 的比例时: 最后调整到 0:100 的比例, 所有的流量将指向 backendB 这时,应用的新版本已经完成部署和切换 (A => B ), 当下一个版本发布时,可以使用相同的步骤,完成 (B => A) 的切换。 OpenNJet 最早是基于 NGINX1.19 基础 fork 并独立演进,随着 NGINX 版本迭代,吸收上游 NGINX 的更新,已经同步更新到 NGINX1.23.1 版本,OpenNJet 具有高性能、稳定、易扩展的特点,同时也解决了 NGINX 长期存在的难于动态配置、管理功能影响业务等问题。 作为底层引擎,OpenNJet 利用动态加载机制可以实现不同的产品形态,如API网关、消息代理、出入向代理,负载均衡,WAF等等。在云原生架构中,OpenNJet 除了提供南北向通信网关的功能以外,还提供了服务网格中东西向通信、透明流量劫持、熔断、遥测与故障注入等新功能特性。 传送门:https://gitee.com/njet-rd

优秀的个人博客,低调大师

Rancher 全球化部署最佳实践

作者 万绍远,CNCF 基金会官方认证 Kubernetes CKA&CKS 工程师,云原生解决方案架构师。对 ceph、Openstack、Kubernetes、prometheus 技术和其他云原生相关技术有较深入的研究。参与设计并实施过多个金融、保险、制造业等多个行业 IaaS 和 PaaS 平台设计和应用云原生改造指导。 “大航海”时代,国内企业纷纷在出海赛道上扬帆起航。伴随着业务出海,系统也要做全球化部署,公有云成为了出海企业的首选。 但是,Kubernetes 在企业落地过程中仍然面临诸多挑战: 需要经验丰富的 IT 人员对 Kubernetes 集群进行部署、升级、监控, 确保系统可靠性。 多个 Kubernetes 集群如何进行统一策略管理、安全管控,实现集中式的可视化管控和一致性运维 ? 如何利用 Kubernetes 和周边生态技术栈,更高效地迭代和交付业务应用 ? Rancher 就是一款很好的工具,能够通过其多集群管理能力和 Kubernetes 生命周期安全支持,帮助出海业务提高部署效率,降低运维成本。 多集群统一纳管 业务出海需要重点评估平滑迁移成本。容器是非常好的应用载体,诸多企业考虑在海外公有云使用 Kubernetes。那么应该选择公有云的虚拟机自建 Kubernetes?还是直接使用公有云厂商的 Kubernetes 发行版,例如 AWS EKS 或者 Azure AKS 呢? 建议企业使用公有云的 Kubernetes 发行版,因为它直接与公有云的基础设施进行了集成,可以更好地利用云上资源,提高部署效率,降低运维成本。以 AWS EKS 为例,使用 EKS 创建 Loadblance 类型的 Service 会自动在 AWS 中创建 ELB 负载均衡器,存储自动对接了 EBS 和 EFS。相比自建 Kubernetes 来说,降低了手动对接的操作过程。 Rancher 支持国内外主流的公有云 Kubernetes 发行版,如 EKS、GKE、ACK、CCE、TKE、AKS……用户可通过 Rancher 一键创建并纳管这些云供应商的 Kubernetes 发行版。 以一个客户架构为例 1、在中国本地数据中心部署 Rancher,本地数据中心的 Kubernetes 使用 RKE 进行部署,并同时建设同城灾备环境。 2、海外业务集群使用 Rancher 对接 AWS,创建对应的 EKS 集群进行统一纳管,通过专线连接。 使用 Rancher 部署 EKS 集群示例 AWS 配置: 1、在 AWS 中提前创建 VPC 和安全组 2、添加 IAM 权限,创建策略:https://docs.ranchermanager.rancher.io/zh/reference-guides/amazon-eks-permissions/minimum-eks-permissions 3、创建用户关联此策略 Rancher 配置 1、创建集群 2、选择 EKS 3、选择对应的区域和配置信息 填写生成的账户 Access-key 和 Secret-key 4、配置 VPC 和子网 5、配置节点主机规格 6、集群创建完成 对应的 AWS 页面也能看见创建的集群。在 Rancher UI 创建负载均衡和创建 PVC 也会自动在 AWS 中创建 多集群统一发布 跨集群应用克隆 当实现多集群纳管后也带来一个问题:用户在测试集群部署服务验证可用以后,如何快速地将应用直接发布到生产集群?如果只能在生产集群中手动创建应用,在手动配置过程中容易出现参数丢失等问题。 为了解决这些问题,Rancher 企业版从设计之初就新增了跨集群应用克隆的功能,可以一键将某一集群的应用发布到其他集群,减少用户手动配置的工作量。 同时用户还可以通过跨集群应用克隆功能,提前将应用备份到其他集群,一旦有集群崩溃的情况出现,可以马上切换流量到其他集群提供服务,快速实现应用的恢复。 Gitops 统一发布 同时 Rancher 也内置了 Gitops 工具 Fleet,实现海量集群同步分发 将应用代码和构建 Docker 镜像的 Dockerfile 文件放置到 Gitlab 对应项目中 在 Gitlab 中创建用于专门用于存放部署 yaml 的项目 配置 CI 工具用于代码编译镜像构建和业务 yaml 文件修改 配置 Rancher-Fleet 检测存放部署 yaml 的项目,有更新后自动部署到对应环境中 点击持续交付功能创建 Fleet 规则 配置对接 git 仓库存放应用部署 yaml 的路径 可以选择部署到全部集群还是指定集群,或通过标签灵活定义的集群组中 完成后可以看见应用部署的状态,后续也会实时检测 git 仓库中的变化,进行自动部署 多集群监控 每个集群可以部署独立的 Prometheus,对单独集群进行监控,可支持对容器云平台以下维度的监控: 集群总体资源使用情况 节点资源使用情况 组件性能监控 应用容器 POD 资源使用监控 但在多集群场景下,单独集群监控需要一个一个点进去,并且人工分析数据,更大的作用是故障后的问题排查,并不能很好地提前发现问题。更好的处理方式是将纳管的全部集群监控数据进行汇总和分析展示。如:内存、CPU、网络流量最高的 top 10(集群、主机、POD);重启次数最多的 top 10 POD;全部集群 Error 事件统一展示;以便更好地帮助平台运维提前发现风险点。 全局监控主要通过 Thanos 实现,会在每个集群的 Prometheus 上通过 Thanos sidecar。对于单个集群的短期数据,Prometheus 通过 local-pv 存储到本地磁盘;而长期数据则通过 Thanos-sidecar 存储到 s3 协议的对象存储中。 安全 随着 Kubernetes 在业务中的广泛使用,容器安全问题正逐步受到重视。容器云平台的安全涉及到镜像安全、集群安全以及容器运行时安全,同时也涉及到租户网络隔离、用户及用户权限控制。Rancher 集成了容器安全平台 SUSE NeuVector,可以更好地保护用户的平台安全。 NeuVector 本身也支持多集群管理、策略统一下发和规则统一管理。 审计日志 为了满足我国本土用户需求,Rancher 企业版在 UI上集成了多维度审计日志展示功能(什么人在什么时间操作了什么资源对象,结果是什么)。通过审计日志,平台管理员可以快速查看到平台的操作记录,方便进行审计。 准入策略控制及网络微隔离 SUSE NeuVector 是业界首个 100% 开源的零信任容器安全平台,在 Rancher 新版本中已经进行了集成,可直接部署使用。NeuVector 可实现以下功能 1、准入策略控制 进入NeuVector进行配置 2、在准入控制菜单添加以下策略 由于容器运行期间会共享宿主机的内核、存储和端口,所以在实际生产环境中,误操作或平台被入侵将影响宿主机上其他应用 Pod 的正常运行,因此需要针对集群进行 Pod 的安全策略控制,以此来保证主机安全。 禁止使用特权容器 禁止从父进程获取更多权限 限制使用主机 IPC 限制只能只读根文件系统 限制 HostPath 路径 限制 HostPort 使用范围 3、网络动态微隔离 集群内 POD 间需要进行网络微隔离,提高安全性,避免 POD 被入侵后互相影响。NeuVector 将每个 workload 识别为一个组,通过对组进行策略控制,并且每个组默认会自动学习对应的网络连接规则和启动进程,并生成白名单。 NeuVector 的组支持 3 种模式:学习模式、监控模式和保护模式。各个模式实现作用如下。 学习模式:学习和记录容器、主机间网络连接情况和进程执行信息。自动构建网络规则白名单,保护应用网络正常行为。为每个服务的容器中运行的进程设定安全基线,并创建进程配置文件规则白名单。 监控模式:NeuVector 监视容器和主机的网络和进程运行情况,遇到非学习模式下记录的行为将在 NeuVector 中进行告警。监控模式不会消耗资源。 保护模式:NeuVector 监视容器和主机的网络和进程运行情况,遇到非学习模式下记录的行为直接拒绝。保护模式是直接拒绝非白名单的访问请求,执行器需要 CPU 和内存通过深度数据包检查来过滤连接,进行判断处理,所以会消耗更多的 CPU 资源。 新建的容器业务自动发现后默认为学习模式,也可以通过设置将默认模式设置为监控模式或保护模式。 总结 总之,Rancher 的多集群管理能力(多集群管理、多集群应用统一发布、多集群监控)和 Kubernetes 生命周期安全支持可以帮助企业提升部署效率,降低运维成本,是出海企业进行全球化部署的明智选择。

优秀的个人博客,低调大师

MySQL 中存储时间的最佳实践

平时开发中经常需要记录时间,比如用于记录某条记录的创建时间以及修改时间。在数据库中存储时间的方式有很多种,比如 MySQL 本身就提供了日期类型,比如 DATETIME,TIMESTAMEP 等,我们也可以直接存储时间戳为 INT 类型,也有人直接将时间存储为字符串类型。 那么到底哪种存储时间的方式更好呢? 不要使用字符串存储时间类型 这是初学者很容易犯的错误,容易直接将字段设置为 VARCHAR 类型,存储"2021-01-01 00:00:00"这样的字符串。当然这样做的优点是比较简单,上手快。 但是极力不推荐这样做,因为这样做有两个比较大的问题: 字符串占用的空间大 这样存储的字段比较效率太低,只能逐个字符比较,无法使用 MySQL 提供的日期API MySQL 中的日期类型 MySQL 数据库中常见的日期类型有 YEAR、DATE、TIME、DATETIME、TIMESTAMEP。因为一般都需要将日期精确到秒,其中比较合适的有DATETIME,TIMESTAMEP。 DATETIME DATETIME 在数据库中存储的形式为:YYYY-MM-DD HH:MM:SS,固定占用 8 个字节。 从 MySQL 5.6 版本开始,DATETIME 类型支持毫秒,DATETIME(N) 中的 N 表示毫秒的精度。例如,DATETIME(6) 表示可以存储 6 位的毫秒值。 TIMESTAMEP TIMESTAMP 实际存储的内容为‘1970-01-01 00:00:00’到现在的毫秒数。在 MySQL 中,由于类型 TIMESTAMP 占用 4 个字节,因此其存储的时间上限只能到‘2038-01-19 03:14:07’。 从 MySQL 5.6 版本开始,类型 TIMESTAMP 也能支持毫秒。与 DATETIME 不同的是,若带有毫秒时,类型 TIMESTAMP 占用 7 个字节,而 DATETIME 无论是否存储毫秒信息,都占用 8 个字节。 类型 TIMESTAMP 最大的优点是可以带有时区属性,因为它本质上是从毫秒转化而来。如果你的业务需要对应不同的国家时区,那么类型 TIMESTAMP 是一种不错的选择。比如新闻类的业务,通常用户想知道这篇新闻发布时对应的自己国家时间,那么 TIMESTAMP 是一种选择。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。 TIMESTAMP 的性能问题 TIMESTAMP 还存在潜在的性能问题。 虽然从毫秒数转换到类型 TIMESTAMP 本身需要的 CPU 指令并不多,这并不会带来直接的性能问题。但是如果使用默认的操作系统时区,则每次通过时区计算时间时,要调用操作系统底层系统函数 __tz_convert(),而这个函数需要额外的加锁操作,以确保这时操作系统时区没有修改。所以,当大规模并发访问时,由于热点资源竞争,会产生两个问题: 性能不如 DATETIME:DATETIME 不存在时区转化问题。 性能抖动:海量并发时,存在性能抖动问题。 为了优化 TIMESTAMP 的使用,建议使用显式的时区,而不是操作系统时区。比如在配置文件中显示地设置时区,而不要使用系统时区: [mysqld] time_zone = "+08:00" 简单总结一下这两种数据类型的优缺点: DATETIME 没有存储的时间上限,而TIMESTAMP存储的时间上限只能到‘2038-01-19 03:14:07’ DATETIME 不带时区属性,需要前端或者服务端处理,但是仅从数据库保存数据和读取数据而言,性能更好 TIMESTAMP 带有时区属性,但是每次需要通过时区计算时间,并发访问时会有性能问题 存储 DATETIME 比 TIMESTAMEP 多占用一部分空间 数值型时间戳(INT) 很多时候,我们也会使用 int 或者 bigint 类型的数值也就是时间戳来表示时间。 这种存储方式的具有 Timestamp 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。 如果需要查看某个时间段内的数据 select * from t where created_at > UNIX_TIMESTAMP('2021-01-01 00:00:00'); DATETIME vs TIMESTAMP vs INT,怎么选? 每种方式都有各自的优势,下面再对这三种方式做一个简单的对比: TIMESTAMP 与 INT 本质一样,但是相比而言虽然 INT 对开发友好,但是对 DBA 以及数据分析人员不友好,可读性差。所以《高性能 MySQL 》的作者推荐 TIMESTAMP 的原因就是它的数值表示时间更加直观。下面是原文: 至于时区问题,可以由前端或者服务这里做一次转化,不一定非要在数据库中解决。 总结 本文比较了几种最常使用的存储时间的方式,我最推荐的还是 DATETIME。理由如下: TIMESTAMP 比数值型时间戳可读性更好 DATETIME 的存储上限为 9999-12-31 23:59:59,如果使用 TIMESTAMP,则 2038 年需要考虑解决方案 DATETIME 由于不需要时区转换,所以性能比 TIMESTAMP 好 如果需要将时间存储到毫秒,TIMESTAMP 要 7 个字节,和 DATETIME 8 字节差不太多 推荐阅读 技术选型:为什么批处理我们却选择了Flink Redis 存储对象信息是用 Hash 还是 String

优秀的个人博客,低调大师

Doris 最佳实践-Compaction调优(2)

本文是 Compaction 调优系列文章的第二篇。在[前一篇文章]中我们介绍了Compaction的一些基本概念。这里我们回顾下两个重要概念: 每个 BE 节点上的 Compaction 操作都是独立进行的。Compaction 的对象是单个 BE 节点上的全部数据分片。 Compaction 分为 Base Compaction(BC) 和 Cumulative Compaction(CC),由Cumulative Point(CP) 划分,根据一定策略,选择一组rowset进行Compaction。 本文将继续从以下两个方面深入了解 Compaction Compaction 机制是如何挑选数据分片进行 Compaction 的。 对于一个数据分片,Compaction 机制是如何确定哪些数据版本参与 Compaction 的。 数据分片选择策略 Compaction 的目的是合并多个数据版本,一是避免在读取时大量的 Merge 操作,二是避免大量的数据版本导致的随机IO。因此,Compaction 策略的重点问题,就是如何选择合适的 tablet,以保证节点上不会出现数据版本过多的数据分片。 Compaction 分数 一个自然的想法,就是每次都选择数据版本最多的数据分片进行 Compaction。这个策略也是 Doris 的默认策略。这个策略在大部分场景下都能很好的工作。但是考虑到一种情况,就是版本多的分片,可能并不是最频繁访问的分片。而 Compaction 的目的就是优化读性能。那么有可能某一张 “写多读少” 表一直在 Compaction,而另一张 “读多写少”的表不能及时的 Compaction,导致读性能变差。 因此,Doris 在选择数据分片时还引入了 “读取频率” 的因素。“读取频率” 和 “版本数量”会根据各自的权重,综合计算出一个 Compaction 分数,分数越高的分片,优先做 Compaction。这两个因素的权重由以下 BE 参数控制(取值越大,权重越高): compaction_tablet_scan_frequency_factor:“读取频率” 的权重值,默认为 0。 compaction_tablet_compaction_score_factor:“版本数量” 的权重,默认为 1。 “读取频率” 的权重值默认为0,即默认仅考虑“版本数量” 这个因素。_ 生产者与消费者 Compaction 是一个 生产者-消费者模型。由一个生产者线程负责选择需要做 Compaction 的数据分片,而多个消费者负责执行 Compaction 操作。 生产者线程只有一个,会定期扫描所有 tablet 来选择合适的 compaction 对象。因为 Base Compaction 和 Cumulative Compaction 是不同类型的任务,因此目前的策略是每生成 9 个 CC 任务,生成一个 BC 任务。任务生成的频率由以下两个参数控制: cumulative_compaction_rounds_for_each_base_compaction_round:多少个CC任务后生成一个BC任务。 generate_compaction_tasks_min_interval_ms:任务生成的间隔。 这两个参数通常情况下不需要调整。_ 生产者线程产生的任务会被提交到消费者线程池。因为 Compaction 是一个IO密集型的任务,为了保证 Compaction 任务不会过多的占用IO资源,Doris 限制了每个磁盘上能够同时进行的 Compaction 任务数量,以及节点整体的任务数量,这些限制由以下参数控制: compaction_task_num_per_disk:每个磁盘上的任务数,默认为2。该参数必须大于等于2,以保证 BC 和 CC 任务各自至少有一个线程。 max_compaction_threads:消费者线程,即Compaction线程的总数。默认为 10。 举个例子,假设一个 BE 节点配置了3个数据目录(即3块磁盘),每个磁盘上的任务数配置为2,总线程数为5。则同一时间,最多有5个 Compaction 任务在进行,而每块磁盘上最多有2个任务在进行。并且最多有3个 BC 任务在进行,因为每块盘上会自动预留一个线程给CC任务。 另一方面,Compaction 任务同时也是一个内存密集型任务,因为其本质是一个多路归并排序的过程,每一路是一个数据版本。如果一个 Compaction 任务涉及的数据版本很多,则会占用更多的内存,如果仅限制任务数,而不考虑任务的内存开销,则有可能导致系统内存超限。因此,Doris 在上述任务个数限制之外,还增加了一个任务配额限制: total_permits_for_compaction_score:Compaction 任务配额,默认 10000。 每个 Compaction 任务都有一个配额,其数值就是任务涉及的数据版本数量。假设一个任务需要合并100个版本,则其配额为100。当正在运行的任务配额总和超过配置后,新的任务将被拒绝。 三个配置共同决定了节点所能承受的 Compaction 任务数量。 数据版本选择策略 一个 Compaction 任务对应的是一个数据分片(Tablet)。消费线程拿到一个 Compaction 任务后,会根据 Compaction 的任务类型,选择 tablet 中合适的数据版本(Rowset)进行数据合并。下面分别介绍 Base Compaction 和 Cumulative Compaction 的数据分片选择策略。 Base Compaction 前文说过,BC 任务是增量数据和基线数据的合并任务。并且只有比 Cumulative Point(CP) 小的数据版本才会参与 BC 任务。因此,BC 任务的数据版本选取策略比较简单。 首先,会选取所有版本在0 到 CP之间的 rowset。然后根据以下几个配置参数,判断是否启动一个 BC 任务: base_compaction_num_cumulative_deltas:一次 BC 任务最小版本数量限制。默认为5。该参数主要为了避免过多 BC 任务。当数据版本数量较少时,BC 是没有必要的。 base_compaction_interval_seconds_since_last_operation:第一个参数限制了当版本数量少时,不会进行 BC 任务。但我们需要避免另一种情况,即某些 tablet 可能仅会导入少量批次的数据,因此当 Doris 发现一个 tablet 长时间没有执行过 BC 任务时,也会触发 BC 任务。这个参数就是控制这个时间的,默认是 86400,单位是秒。 以上两个参数通常情况下不需要修改,在某些情况下如何需要想尽快合并基线数据,可以尝试改小 base_compaction_num_cumulative_deltas 参数。但这个参数只会影响到 “被选中的 tablet”。而 “被选中” 的前提是这个 tablet 的数据版本数量是最多的。_ Cumulative Compaction CC 任务只会选取版本比 CP 大的数据版本。其本身的选取策略也比较简单,即从 CP 版本开始,依次向后选取数据版本。最终的数据版本集合由以下参数控制: min_cumulative_compaction_num_singleton_deltas:一次 CC 任务最少的版本数量限制。这个配置是和 cumulative_size_based_compaction_lower_size_mbytes 配置同时判断的。即如果版本数量小于阈值,并且数据量也小于阈值,则不会触发 CC 任务。以避免躲过不比较的 CC 任务。默认是5。 max_cumulative_compaction_num_singleton_deltas:一次 CC 任务最大的版本数量限制。以防止一次 CC 任务合并的版本数量过多,占用过多资源。默认是1000。 cumulative_size_based_compaction_lower_size_mbytes:一次 CC 任务最少的数据量,和min_cumulative_compaction_num_singleton_delta 同时判断。默认是 64,单位是 MB。 简单来说,默认配置下,就是从 CP 版本开始往后选取 rowset。最少选5个,最多选 1000 个,然后判断数据量是否大于阈值即可。 CC 任务还有一个重要步骤,就是在合并任务结束后,设置新的 Cumulative Point。CC 任务合并完成后,会产生一个合并后的新的数据版本,而我们要做的就是判断这个新的数据版是 “晋升” 到 BC 任务区,还是依然保留在 CC 任务区。举个例子: 假设当前 CP 是 10。有一个 CC 任务合并了 [10-13] [14-14] [15-15] 后生成了 [10-15] 这个版本。如果决定将 [10-15] 版本移动到 BC 任务区,则需修改 CP 为 15,否则 CP 保持不变,依然为 10。 CP 只会增加,不会减少。以下参数决定了是否更新 CP: cumulative_size_based_promotion_ratio:晋升比率。默认 0.05。 cumulative_size_based_promotion_min_size_mbytes:最小晋升大小,默认 64,单位 MB。 cumulative_size_based_promotion_size_mbytes:最大晋升大小,默认 1024,单位 MB。 以上参数比较难理解,这里我们先解释下 “晋升” 的原则。一个 CC 任务生成的 rowset 的晋升原则,是其数据大小和基线数据的大小在 “同一量级”。这个类似 2048 小游戏,只有相同的数字才能合并形成更大的数字。而上面三个参数,就是用于判断一个新的rowset是否匹配基线数据的数量级。举例说明: 在默认配置下,假设当前基线数据(即所有 CP 之前的数据版本)的数据量为 10GB,则晋升量级为 (10GB * 0.05)512MB。这个数值大于 64 MB 小于 1024 MB,满足条件。所以如果 CC 任务生成的新的 rowset 的大小大于 512 MB,则可以晋升,即 CP 增加。而假设基线数据为 50GB,则晋升量级为(50GB * 0.05)2.5GB。这个数值大于 64 MB 也大于 1024 MB,因此晋升量级会被调整为 1024 MB。所以如果 CC 任务生成的新的 rowset 的大小大于 1024 MB,则可以晋升,即 CP 增加。 从上面的例子可以看出,cumulative_size_based_promotion_ratio 用于定义 “同一量级”,0.05 即表示数据量大于基线数据的 5% 的 rowset 都有晋升的可能,而 cumulative_size_based_promotion_min_size_mbytes 和 cumulative_size_based_promotion_size_mbytes 用于保证晋升不会过于频繁或过于严格。 >这三个参数会直接影响 BC 和 CC 任务的频率,尤其在高频导入场景下需要适当调整。我们会在后续文章中举例说明。 其他 Compaction 参数和注意事项 还有一些参数和 Compaction 相关,在某些情况下需要修改: disable_auto_compaction:默认为 false,修改为 true 则会禁止 Compaction 操作。该参数仅在一些调试情况,或者 compaction 异常需要临时关闭的情况下才需使用。 Delete灾难 通过 DELETE FROM 语句执行的数据删除操作,在 Doris 中也会生成一个数据版本用于标记删除。这种类型的数据版本比较特殊,我们成为 “删除版本”。删除版本只能通过 Base Compaction 任务处理。因此在在遇到删除版本时,Cumulative Point 会强制增加,将删除版本移动到 BC 任务区。因此数据导入和删除交替发生的场景通常会导致 Compaction 灾难。比如以下版本序列: [0-10] [11-11] 删除版本 [12-12] [13-13] 删除版本 [14-14] [15-15] 删除版本 [16-16] [17-17] 删除版本 .... 在这种情况下,CC 任务几乎不会 被触发(因为CC任务只能选择一个版本,而无法处理删除版本),所有版本都会交给 Base Compaction 处理,导致 Compaction 进度缓慢。目前Doris还无法很好的处理这种场景,因此需要在业务上尽量避免。 未完待续 本文介绍了 Doris Compaction 任务的生成逻辑和执行逻辑。并且介绍了相关控制参数。接下来的文章,将通过一些具体场景来介绍调整 Compaction 参数的思路,以满足业务需求。 【往期回顾】 【Doris Weekly】2020.05.25~2021.06.08 【Doris Weekly】2021.05.10~2021.05.24 【Doris Weekly】2021.04.26~2021.05.09 【精彩文章】 Apache Doris Roadmap 2021 【Doris功能介绍】proc系统 【Doris全面解析】Doris Compaction 机制解析

优秀的个人博客,低调大师

Cassandra 最佳实践系列(1) - CQL QuickStart

简易搭建单节点C* 本文介绍如何简单的搭建一个Cassandra的单节点,以后会介绍如何构建多节点的Cassandra集群;为了搭建一个单节点我们需要做下面几件事情: 1.节点部署JAVA 基本环境;2.获取需要的Cassandra二进制包:可以编译源码获取也可以直接官网下载3.11.5的bin包;3.tar xf apache-cassandra-3.11.5-bin.tar.gz4.cd apache-cassandra-3.11.5/bin目录; 5.执行./cassandra 启动单节点C*; 接下来你在bin目录下面通过执行./nodetool status 如果观测到下面的状态,就证明单节点的Cassandra已经正确启动。 注意: 因为我们没有修改Cassandra的配置文件,这里单节点的进程都是使用默认配置,比如使用默认256的vnode,使用/var/lib/cassandra/data做数据存储目录,使用/var/lib/cassandra/commitlog做commitlog存储目录等, 默认绑定localhost,默认没有账户密码认证。 访问C* 通过cqlsh bin目录下面的cqlsh类似于访问Cassandra的一个client,默认情况指定one级别去访问Cassandra,由于我们此处设置是sever绑定localhost,且cqlsh默认访问localhost,所以直接bin目录下面./cqlsh既可以访问到上面部署的单节点Cassandra,如下图: 我们可以通过cqlsh执行常见的Cassandra DDL、DML操作,比如这里我建一个keyspace ks以及在keyspace里面建一个table tb。分别使用如下cql: CREATE KEYSPACE ks WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}; use ks; CREATE TABLE tb ( name text PRIMARY KEY ,age int); 我们可以在Cassandra里面通过执行: DESCRIBE ks; 获取得到该keyspace下面的所有相关的keyspace、table、index、mv的定义;如下图: 我们也可以在这个表 tb里面执行insert操作以及select操作,如下图: 通过java driver 当然我们实际业务开发的时候很难通过cql去执行一些常见的dml操作,我们这里使用datastax 公司开源的java-driver进行常见的cassandra访问,当然也有其他版本以及语言的driver(见这里). 使用java driver的时候,只需要新建maven项目,然后pom.xml里面添加如下配置(我们这里使用3.8.0的java driver做演示): <dependency> <groupId>com.datastax.cassandra</groupId> <artifactId>cassandra-driver-core</artifactId> <version>3.8.0</version> </dependency> 然后我们的演示代码如下: package com.aliyun.cstar.driver.test; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; public class Test { static String[] CONTACT_POINTS = {"127.0.0.1"}; static int PORT = 9042; public static void main(String[] args) { Cluster cluster = null; try { System.out.println("CLUSTER CONNECT !"); //cluster operation cluster = Cluster.builder().addContactPoints(CONTACT_POINTS).withPort(PORT).build(); Session session = cluster.connect(); System.out.println("CREATE KEYSPACE AND TABLE !"); //DDL:keyspace and table operation session.execute("CREATE KEYSPACE IF NOT EXISTS newks WITH replication " + "= {'class':'SimpleStrategy', 'replication_factor':1};"); session.execute("CREATE TABLE IF NOT EXISTS newks.newtb (name text PRIMARY KEY, age int)"); System.out.println("INSERT INTO TABLE AND SELECT TABLE !"); //DML:insert and select session.execute("INSERT INTO newks.newtb (name, age) VALUES('xla', 22)"); session.execute("INSERT INTO newks.newtb (name, age) VALUES('xlb', 22)"); session.execute("INSERT INTO newks.newtb (name, age) VALUES('xlc', 22)"); ResultSet results = session.execute("SELECT * FROM newks.newtb"); System.out.println(results.all()); results = session.execute("SELECT name from newks.newtb"); System.out.println(results.all()); results = session.execute("SELECT count(*) from newks.newtb"); System.out.println(results.all()); System.out.println("FINISHED OPERATION !"); } finally { if (cluster != null) cluster.close(); } } } 最后的结果如图: CQL使用 这里简单介绍下常见的CQL的使用,主要分2类:DDL以及DML。 DDL : CREATE KEYSPACE: CREATE KEYSPACE ksname WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3}; 如果是多DC的话可以如下建表,保证使用NetworkTopologyStrategy: CREATE KEYSPACE ksname WITH replication = {'class': 'NetworkTopologyStrategy', 'DC1' : 3, 'DC2' : 3}; ALTER KEYSPACE : ALTER KEYSPACE ksname WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 4}; 修改了副本数从3变成4. CREATE TABLE : CREATE TABLE t ( pk text, v1 int, v2 text, v3 text , PRIMARY KEY (pk, v1) ); 必须指定primary key,用来唯一确定数据在集群的唯一id。当然TABLE还有很多别的属性,这里使用默认的,其他的以后再详细介绍。 这里主要介绍我们常见的使用方式,还有drop keyspace,truncate table等等可以见这里。 DML: 我们列举我们常见的SELECT 、INSERT、UPDATE 、DELETE、BATCH操作: INSERT : INSERT INTO t (pk, v1, v2, v3) VALUES ( 'pk1', 1, 'v2', 'v3'); 一定要提供我们的PRIMARY KEY的数据; SELECT : SELECT pk, v1, v2, v3 FROM t; UPDATE: UPDATE t SET v2 = 'vv2' WHERE pk = 'pk1' AND v1 =1; DELETE: DELETE FROM t WHERE pk = 'pk1' AND v1 =1; BATCH: BEGIN BATCH ... INSERT INTO t (pk, v1, v2, v3) VALUES ( 'pk1', 1, 'v2', 'vV3'); ... APPLY BATCH ;

优秀的个人博客,低调大师

Cassandra 最佳实践系列(2) - 选型篇

本文会从cassandra的选型,机器基本配置,节点数,基本使用介绍方面进行基本的介绍; 机器基本配置选择 Cassandra的性能使用可以随着机器的硬件配置,以及集群的节点数的横向和纵向的升级而相应的有所提升。 CPU Cassandra内部会有很多地方使用多线程进行处理,一般配置里面对于读写而言,写操作是CPU bound,所以如果系统的写操作会相对多一点,对cpu的要求也会相对配置要好一点,一般至少是2c起步,如果是生产环境对写要求更高,相对的cpu核数应该更好。 内存 Cassandra使用java 语言编写,会用到jvm-on heap内存以及offheap内存,其中jvm预先想操作系统申请的内存大小是系统大小的1/2, 其中off-heap会使用于压缩元数据,bloom filter等等。官方建议生产环境内存不低于8G,但是具体可以视自己的需求再说,对于gc算法来说: 堆内存小于12G,推荐cms算法; 大于12G堆内存的话,可以使用G1 算法; 磁盘 对于cassandra而言有几个地方需要使用到磁盘,commitlog、hint、cache-file、sstable-file。其中对我们来说,我们需要重点关注commitlog的文件以及sstable的文件,因为写操作会先写commitlog,然后把数据丢到memtable,然后memtable会异步的dump到磁盘成为sstable的文件,而且sstable后台会进行异步的compaction操作合并成新文件。那么这里commitlog的会影响我们的写性能,常见的建议是commitlog的配置 磁盘与放置sstable的data 目录分开配置,commitlog单独配置一块盘,因为写commitlog的速度直接影响写操作的速度,所以建议commitlog的配置磁盘需要稍微好一点,但是容量不需要很大,因为commitlog的数据在相关memtable数据dump到磁盘以后就会删除。只有存留在memtable的数据在commitlog里面以做节点crash以后做replay使用。 存放sstable的磁盘可以使用HDD/SSD磁盘,相关cassandra有优化配置,那么这里的话可以使用多块磁盘组合使用Raid0或者cassandra所谓的JBOD方式,使用其他的Raid1-Raid5不是最优的使用推荐,因为在节点层面有多数据副本冗余。具体磁盘容量视集群业务需求以及其他配置来定。 节点数 Cassandra可以是单节点(需要设置replicat factor 为1),2个节点(replicat factor最多是2),3个节点,…..个节点,理论上的扩容是线性的,无上限的扩容,可以从1 到很大。但是常见一般300个物理节点基本是可以了。

优秀的个人博客,低调大师

云存储网关的缓存最佳实践

前言 云存储网关支持通过传统的文件协议(SMB/NFS)来访问OSS Bucket里面的数据,并能够通过缓存技术将用户频繁访问的热点数据保留在网关侧的缓存盘里,从而提供给用户更好的访问体验。使得用户在享受云上海量OSS存储空间的同时,还兼具本地的高速访问性能。下面是阿里云文件网关的架构图。 用户在使用阿里云云存储网关时,经常会碰到一些缓存相关的问题,比如在创建共享时如何选择缓存盘的容量和类型,比如缓存的数据淘汰策略是什么等。本文接下来的内容将结合缓存盘的工作原理来解开这些困惑。云存储网关根据支持的协议的不同,分为支持NFS/SMB文件协议的文件网关和支持iSCSI协议的块网关。它们两者的缓存工作机制是不一样的,本文主要针对的文件网关。 工作原理 文件网关支持缓存模式和复制模式两种模式,绝大部分用户使用的应该都是缓存模式。缓存模式是指缓存盘的数据到一定比例之后,文件网关会自动淘汰那些访问不频繁的数据。在这种模式下,固定容量的缓存盘可以管理远远大于缓存盘实际容量的OSS Bucket。复制模式则不同,数据在网关侧和OSS Bucket里面是1:1的,所以网关不会去做数据的淘汰,一定容量的缓存盘理论上只能管理对应于缓存盘容量的OSS Bucket。复制模式针对的场景主要是OSS Bucket总数据量基本不会增长且总数据量不是特别大,同时希望将所有数据都保持在网关共享里加速访问。不过这种场景毕竟在少数,绝大多数用户会选择缓存模式以应对日后OSS Bucket里面的数据增长。 在缓存模式下,缓存盘的数据会在60%满的时候触发淘汰直到实际数据量落到60%以下,从而保证永远有足够的缓存容量面对新的数据写入。那么淘汰的策略是如何的呢,如何决定哪些数据是可以淘汰的呢?文件网关淘汰的实际上是已经同步到OSS Bucket里面的文件,也就是说对某个文件的最后一次修改应用到网关的SMB或者NFS共享之后,并且网关已经将这个文件上传到OSS Bucket里面,那么这个文件就是可以淘汰的。如果用户还在持续的对某个文件进行写入,这个文件是不会被选为一个淘汰的对象的。所以用户如果同时打开多个文件进行写入,缓存盘的容量就应该比同时在写的所有文件的总容量要大,否则就有可能导致数据来不及淘汰而造成写入错误!!! 在复制模式下,因为数据不会发生淘汰,相对来说就简单很多。缓存盘的容量比OSS Bucket里面的总数据大就可以,这种模式注定它不可能管理特别大的数据量,因为当前支持的缓存盘的最大容量32TB。所以除非对复制模式有强需求,还是推荐使用缓存模式,相对来说更加灵活。 另外文件网关会预留一部分缓存盘空间存储元数据,一般会预留20%。这部分元数据主要是用来存储单个文件的元数据,包括大小,修改时间等等。所以即使某个文件的数据被淘汰之后,网关还是存储了一个桩文件在元数据里,这样用户从客户端进行文件夹浏览的时候,还是能够看到数据被淘汰掉的这个文件,给用户一致的体验。用户如果试图读取这个文件内容,网关会负责将数据再次从OSS Bucket里获取到缓存盘里面。这部分元数据空间关系到当前共享可以支持的最大文件数目,毕竟40GB的缓存盘的元数据空间肯定低于50GB的缓存盘的元数据空间。一般来说100G的缓存盘已经可以支持到1000万文件了。 注意事项 了解缓存的工作原理之后,下面这些其实都比较好理解了。 如果你的业务对带宽和IOPS的要求比较高,比如跑的数据库之类的对时延要求比较高的业务,那么可能SSD类型的缓存盘更适合,因为它拥有更好的带宽和IOPS。SSD类型的缓存盘的带宽和IOPS都会比高效云盘更优秀,SSD缓存的最高IOPS可以到25000,高效云盘缓存是5000,带宽方面SSD缓存最高可以到300MB,高效云盘缓存的带宽最高可以到140MB。 容量的选择主要和并发数和最大文件大小有关。缓存盘的可用容量(需要扣除元数据空间)需要大于并发数和最大文件大小的乘积,这样才不会造成数据写入错误。当然越大的缓存盘本地能够缓存的热数据量也就越多,总体来说性能会更好。所以如果希望能够获得比较好的性能,缓存盘还是要设置的稍微大一些。 云存储网关的控制台现在也提供了非常方便的计算器,根据用户输入的IOPS和带宽需求等可以作出合适的缓存容量和类型的推荐。结合我们前面讲的对照看下图中每一个条目,相信很好理解。 小结 本文介绍了云存储网关文件网关的缓存工作原理,包括缓存盘元数据空间管理,数据淘汰策略等,旨在解答用户在选择和使用缓存盘时遇到的一些问题。

优秀的个人博客,低调大师

Android 组件化最佳实践 ARetrofit 原理

本文首发于 vivo互联网技术 微信公众号 https://mp.weixin.qq.com/s/TXFt7ymgQXLJyBOJL8F6xg 作者:朱壹飞 ARetrofit 是一款针对Android组件之间通信的路由框架,实现快速组件化开发的利器。本文主要讲述 ARetrofit 实现的原理。 简介 ARetrofit 是一款针对Android组件之间通信的路由框架,实现快速组件化开发的利器。 源码链接:https://github.com/yifei8/ARetrofit 组件化架构 APP Demo, ARetrofit 使用实例:https://github.com/yifei8/HappyNote 组件化 Android组件化已经不是一个新鲜的概念了,出来了已经有很长一段时间了,大家可以自行Google,可以看到一堆相关的文章。 简单的来说,所谓的组件就是Android Studio中的Module,每一个Module都遵循高内聚的原则,通过ARetrofit 来实现无耦合的代码结构,如下图: 每一个 Module 可单独作为一个 project 运行,而打包到整体时 Module 之间的通信通过 ARetrofit 完成。 ARetrofit 原理 讲原理之前,我想先说说为什么要ARetrofit。开发ARetrofit 这个项目的思路来源其实是 Retrofit,Retrofit 是Square公司开发的一款针对 Android 网络请求的框架,这里不对Retrofit展开来讲。主要是 Retrofit 框架使用非常多的设计模式,可以说 Retrofit 这个开源项目将Java的设计模式运用到了极致,当然最终提供的API也是非常简洁的。如此简洁的API,使得我们APP中的网络模块实现变得非常轻松,并且维护起来也很舒服。因此我觉得有必要将Android组件之间的通信也变得轻松,使用者可以优雅的通过简洁的API就可以实现通信,更重要的是维护起来也非常的舒服。 ARetrofit 基本原理可以简化为下图所示: 1.通过注解声明需要通信的Activity/Fragment或者Class 2.每一个module通过annotationProcessor在编译时生成待注入的RouteInject的实现类和AInterceptorInject的实现类。 这一步在执行app[build]时会输出日志,可以直观的看到,如下图所示: 注: AInjecton::Compiler >>> Apt interceptor Processor start... <<< 注: AInjecton::Compiler enclosindClass = null 注: AInjecton::Compiler value = 3 注: AInjecton::Compiler auto generate class = com $$ sjtu $$ yifei $$ eCGVmTMvXG $$ AInterceptorInject 注: AInjecton::Compiler add path= 3 and class= LoginInterceptor .... 注: AInjecton::Compiler >>> Apt route Processor start... <<< 注: AInjecton::Compiler enclosindClass = null 注: AInjecton::Compiler value = /login-module/ILoginProviderImpl 注: AInjecton::Compiler enclosindClass = null 注: AInjecton::Compiler value = /login-module/LoginActivity 注: AInjecton::Compiler enclosindClass = null 注: AInjecton::Compiler value = /login-module/Test2Activity 注: AInjecton::Compiler enclosindClass = null 注: AInjecton::Compiler value = /login-module/TestFragment 注: AInjecton::Compiler auto generate class = com $$ sjtu $$ yifei $$ VWpdxWEuUx $$ RouteInject 注: AInjecton::Compiler add path= /login-module/TestFragment and class= null 注: AInjecton::Compiler add path= /login-module/LoginActivity and class= null 注: AInjecton::Compiler add path= /login-module/Test2Activity and class= null 注: AInjecton::Compiler add path= /login-module/ILoginProviderImpl and class= null 注: AInjecton::Compiler >>> Apt route Processor succeed <<< 3.将编译时生成的类注入到RouterRegister中,这个类主要用于维护路由表和拦截器,对应的[build]日志如下: TransformPluginLaunch >>> ========== Transform scan start =========== TransformPluginLaunch >>> ========== Transform scan end cost 0.238 secs and start inserting =========== TransformPluginLaunch >>> Inserting code to jar >> /Users/yifei/as_workspace/ARetrofit/app/build/intermediates/transforms/TransformPluginLaunch/release/8.jar TransformPluginLaunch >>> to class >> com/sjtu/yifei/route/RouteRegister.class InjectClassVisitor >>> inject to class: InjectClassVisitor >>> com/sjtu/yifei/route/RouteRegister{ InjectClassVisitor >>> public *** init() { InjectClassVisitor >>> register("com.sjtu.yifei.FBQWNfbTpY.com $$ sjtu $$ yifei $$ FBQWNfbTpY $$ RouteInject") InjectClassVisitor >>> register("com.sjtu.yifei.klBxerzbYV.com $$ sjtu $$ yifei $$ klBxerzbYV $$ RouteInject") InjectClassVisitor >>> register("com.sjtu.yifei.JmhcMMUhkR.com $$ sjtu $$ yifei $$ JmhcMMUhkR $$ RouteInject") InjectClassVisitor >>> register("com.sjtu.yifei.fpyxYyTCRm.com $$ sjtu $$ yifei $$ fpyxYyTCRm $$ AInterceptorInject") InjectClassVisitor >>> } InjectClassVisitor >>> } TransformPluginLaunch >>> ========== Transform insert cost 0.017 secs end =========== 4.Routerfit.register(Class service) 这一步主要是通过动态代理模式实现接口中声明的服务。 前面讲的是整体的框架设计思想,便于读者从全局的觉得来理解ARetrofit的框架的架构。接下来,将待大家个个击破上面提到的annotationProcessor、 transform在项目中如何使用,以及动态代理、拦截器功能的实现等细节。 一、annotationProcessor生成代码 annotationProcessor(注解处理器)是javac内置的一个用于编译时扫描和处理注解(Annotation)的工具。简单的说,在源代码编译阶段,通过注解处理器,我们可以获取源文件内注解(Annotation)相关内容。Android Gradle 2.2 及以上版本提供annotationProcessor的插件。 在ARetrofit中annotationProcessor对应的module是auto-complier,在使用annotationProcessor之前首先需要声明好注解。关于注解不太了解或者遗忘的同学可直接参考我之前写的Java注解这篇文章,本项目中声明的注解在auto-annotation这个module中,主要有: @Extra 路由参数 @Flags intent flags @Go 路由路径key @Interceptor 声明自定义拦截器 @RequestCode 路由参数 @Route路由 @Uri @IMethod 用于标记注册代码将插入到此方法中(transform中使用) @Inject 用于标记需要被注入类,最近都将插入到标记了#com.sjtu.yifei.annotation.IMethod的方法中(transform中使用) 创建自定义的注解处理器,具体使用方法可参考利用注解动态生成代码,本项目中的注解处理器如下所示: //这是用来注册注解处理器要处理的源代码版本。 @SupportedSourceVersion(SourceVersion.RELEASE_8) //这个注解用来注册注解处理器要处理的注解类型。有效值为完全限定名(就是带所在包名和路径的类全名 @SupportedAnnotationTypes({ANNOTATION_ROUTE, ANNOTATION_GO}) //来注解这个处理器,可以自动生成配置信息 @AutoService(Processor.class) public class IProcessor extends AbstractProcessor { } 生成代码的关键部分在GenerateAInterceptorInjectImpl 和GenerateRouteInjectImpl中,以下贴出关键代码: public void generateAInterceptorInjectImpl(String pkName) { try { String name = pkName.replace(".",DECOLLATOR) + SUFFIX; logger.info(String.format("auto generate class = %s", name)); TypeSpec.Builder builder = TypeSpec.classBuilder(name) .addModifiers(Modifier.PUBLIC) .addAnnotation(Inject.class) .addSuperinterface(AInterceptorInject.class); ClassName hashMap = ClassName.get("java.util", "HashMap"); //Map<String, Class<?>> TypeName wildcard = WildcardTypeName.subtypeOf(Object.class); TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard); TypeName string = ClassName.get(Integer.class); TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny); MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getAInterceptors") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(map) .addStatement("$T interceptorMap = new $T<>()", map, hashMap); for (Map.Entry<Integer, ClassName> entry : interceptorMap.entrySet()) { logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().simpleName()); injectBuilder.addStatement("interceptorMap.put($L, $T.class)", entry.getKey(), entry.getValue()); } injectBuilder.addStatement("return interceptorMap"); builder.addMethod(injectBuilder.build()); JavaFile javaFile = JavaFile.builder(pkName, builder.build()) .build(); javaFile.writeTo(filer); } catch (Exception e) { e.printStackTrace(); } } public void generateRouteInjectImpl(String pkName) { try { String name = pkName.replace(".",DECOLLATOR) + SUFFIX; logger.info(String.format("auto generate class = %s", name)); TypeSpec.Builder builder = TypeSpec.classBuilder(name) .addModifiers(Modifier.PUBLIC) .addAnnotation(Inject.class) .addSuperinterface(RouteInject.class); ClassName hashMap = ClassName.get("java.util", "HashMap"); //Map<String, String> TypeName wildcard = WildcardTypeName.subtypeOf(Object.class); TypeName classOfAny = ParameterizedTypeName.get(ClassName.get(Class.class), wildcard); TypeName string = ClassName.get(String.class); TypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), string, classOfAny); MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("getRouteMap") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(map) .addStatement("$T routMap = new $T<>()", map, hashMap); for (Map.Entry<String, ClassName> entry : routMap.entrySet()) { logger.info("add path= " + entry.getKey() + " and class= " + entry.getValue().enclosingClassName()); injectBuilder.addStatement("routMap.put($S, $T.class)", entry.getKey(), entry.getValue()); } injectBuilder.addStatement("return routMap"); builder.addMethod(injectBuilder.build()); JavaFile javaFile = JavaFile.builder(pkName, builder.build()) .build(); javaFile.writeTo(filer); } catch (Exception e) { e.printStackTrace(); } } 二、Transform Android Gradle 工具在 1.5.0 版本后提供了 Transfrom API, 允许第三方 Plugin在打包dex文件之前的编译过程中操作 .class 文件。这一部分面向高级Android工程师的,面向字节码编程,普通工程师可不做了解。 写到这里也许有人会有这样一个疑问,既然annotationProcessor这么好用为什么还有Transform面向字节码注入呢?这里需要解释以下,annotationProcessor具有局限性,annotationProcessor只能扫描当前module下的代码,且对于第三方的jar、aar文件都扫描不到。而Transform就没有这样的局限性,在打包dex文件之前的编译过程中操作.class 文件。 关于Transfrom API在Android Studio中如何使用可以参考Transform API — a real world example,顺便提供一下字节码指令方便我们读懂ASM。 本项目中的Transform插件在AInject中,实现源码TransformPluginLaunch如下,贴出关键部分: /** * * 标准transform的格式,一般实现transform可以直接拷贝一份重命名即可 * * 两处todo实现自己的字节码增强/优化操作 */ class TransformPluginLaunch extends Transform implements Plugin<Project> { @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation) //todo step1: 先扫描 transformInvocation.inputs.each { TransformInput input -> input.jarInputs.each { JarInput jarInput -> ... } input.directoryInputs.each { DirectoryInput directoryInput -> //处理完输入文件之后,要把输出给下一个任务 ... } } //todo step2: ...完成代码注入 if (InjectInfo.get().injectToClass != null) { ... } } /** * 扫描jar包 * @param jarFile */ static void scanJar(File jarFile, File destFile) { } /** * 扫描文件 * @param file */ static void scanFile(File file, File dest) { ... } } 注入代码一般分为两个步骤: 第一步:扫描 这一部分主要是扫描的内容有: 注入类和方法的信息,是AutoRegisterContract的实现类和其中@IMethod,@Inject的方法。 待注入类的和方法信息,是RouteInject 和 AInterceptorInject实现类且被@Inject注解的。 第二步:注入 以上扫描的结果,将待注入类注入到注入类的过程。这一过程面向ASM操作,可参考字节码指令来读懂以下的关键注入代码: class InjectClassVisitor extends ClassVisitor { ... class InjectMethodAdapter extends MethodVisitor { InjectMethodAdapter(MethodVisitor mv) { super(Opcodes.ASM5, mv) } @Override void visitInsn(int opcode) { Log.e(TAG, "inject to class:") Log.e(TAG, own + "{") Log.e(TAG, " public *** " + InjectInfo.get().injectToMethodName + "() {") if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) { InjectInfo.get().injectClasses.each { injectClass -> injectClass = injectClass.replace('/', '.') Log.e(TAG, " " + method + "(\"" + injectClass + "\")") mv.visitVarInsn(Opcodes.ALOAD, 0) mv.visitLdcInsn(injectClass) mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, own, method, "(Ljava/lang/String;)V", false) } } Log.e(TAG, " }") Log.e(TAG, "}") super.visitInsn(opcode) } ... } ... } 三、动态代理 定义:为其它对象提供一种代理以控制对这个对象的访问控制;在某些情况下,客户不想或者不能直接引用另一个对象,这时候代理对象可以在客户端和目标对象之间起到中介的作用。 Routerfit.register(Class service) 这里就是采用动态代理的模式,使得ARetrofit的API非常简洁,使用者可以优雅定义出路由接口。关于动态代理的学习难度相对来说还比较小,想了解的同学可以参考这篇文章java动态代理。 本项目相关源码: public final class Routerfit { ... private <T> T create(final Class<T> service) { RouterUtil.validateServiceInterface(service); return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable { // If the method is a method from Object then defer to normal invocation. if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } ServiceMethod<Object> serviceMethod = (ServiceMethod<Object>) loadServiceMethod(method, args); if (!TextUtils.isEmpty(serviceMethod.uristring)) { Call<T> call = (Call<T>) new ActivityCall(serviceMethod); return call.execute(); } try { if (serviceMethod.clazz == null) { throw new RouteNotFoundException("There is no route match the path \"" + serviceMethod.routerPath + "\""); } } catch (RouteNotFoundException e) { Toast.makeText(ActivityLifecycleMonitor.getApp(), e.getMessage(), Toast.LENGTH_SHORT).show(); e.printStackTrace(); } if (RouterUtil.isSpecificClass(serviceMethod.clazz, Activity.class)) { Call<T> call = (Call<T>) new ActivityCall(serviceMethod); return call.execute(); } else if (RouterUtil.isSpecificClass(serviceMethod.clazz, Fragment.class) || RouterUtil.isSpecificClass(serviceMethod.clazz, android.app.Fragment.class)) { Call<T> call = new FragmentCall(serviceMethod); return call.execute(); } else if (serviceMethod.clazz != null) { Call<T> call = new IProviderCall<>(serviceMethod); return call.execute(); } if (serviceMethod.returnType != null) { if (serviceMethod.returnType == Integer.TYPE) { return -1; } else if (serviceMethod.returnType == Boolean.TYPE) { return false; } else if (serviceMethod.returnType == Long.TYPE) { return 0L; } else if (serviceMethod.returnType == Double.TYPE) { return 0.0d; } else if (serviceMethod.returnType == Float.TYPE) { return 0.0f; } else if (serviceMethod.returnType == Void.TYPE) { return null; } else if (serviceMethod.returnType == Byte.TYPE) { return (byte)0; } else if (serviceMethod.returnType == Short.TYPE) { return (short)0; } else if (serviceMethod.returnType == Character.TYPE) { return null; } } return null; } }); } ... } 这里ServiceMethod是一个非常重要的类,使用了外观模式,主要用于解析方法中的被注解所有信息并保存起来。 四、拦截器链实现 本项目中的拦截器链设计,使得使用者可以非常优雅的处理业务逻辑。如下: @Interceptor(priority = 3) public class LoginInterceptor implements AInterceptor { private static final String TAG = "LoginInterceptor"; @Override public void intercept(final Chain chain) { //Test2Activity 需要登录 if ("/login-module/Test2Activity".equalsIgnoreCase(chain.path())) { Routerfit.register(RouteService.class).launchLoginActivity(new ActivityCallback() { @Override public void onActivityResult(int i, Object data) { if (i == Routerfit.RESULT_OK) {//登录成功后继续执行 Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登录成功", Toast.LENGTH_LONG).show(); chain.proceed(); } else { Toast.makeText(ActivityLifecycleMonitor.getTopActivityOrApp(), "登录取消/失败", Toast.LENGTH_LONG).show(); } } }); } else { chain.proceed(); } } } 这一部分实现的思想是参考了okhttp中的拦截器,这里使用了java设计模式责任链模式,具体实现欢迎阅读源码。 总结 基本上读完本文可以对 ARetrofit 的核心原理有了很清晰的理解.简单来说 ARetrofit 通过 annotationProcessor 在编译时获取路由相关内容,通过 ASM 实现了可跨模块获取对象,最终通过动态代理实现面向切面编程(AOP)。 ARetrofit 相对于其他同类型的路由框架来说,其优点是提供了更加简洁的 API,其中高阶用法对开发者提供了更加灵活扩展方式,开发者还可以结合 RxJava 完成复杂的业务场景。具体可以参考 ARetrofit 的基本用法,以及 Issues。 ———— 参考资料 ———— Java注解:https://www.jianshu.com/p/ef1146a771b5 利用注解动态生成代码:https://blog.csdn.net/Gaugamela/article/details/79694302 Transform API — a real world example:https://medium.com/grandcentrix/transform-api-a-real-world-example-cfd49990d3e1

优秀的个人博客,低调大师

分析网络超时问题的最佳实践

对于云上的用户来说,业务日志里面报超时问题处理起来往往比价棘手,因为1) 问题点可能在云基础设施层,也有可能在业务软件层,需要排查的范围非常广;2) 这类问题往往是不可复现问题,抓到现场比较难。在本文里就分析下如何来分辨和排查这类问题的根本原因。 业务超时 != 网络丢包 由于业务的形态不同,软件实现语言和框架的不同,业务日志中打印出的信息可能是各不相同,比如如下关键字: "SocketTimeOut", "Read timed out", "Request timeout" 等 从形式看都属于网络超时这一类,但是需要明确一个概念:这类问题是发生的原因是请求超过了设定的timeout时间,这个设置有可能来自客户端,服务器端或者网络中间节点,这是直接原因。网络丢包可能会导致超时,但是并不是充分条件。总结业务超时和网络丢包的关系如下: 网络丢包

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册