首页 文章 精选 留言 我的

精选列表

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

Kubernetes 集群无损升级实践

一、背景 活跃的社区和广大的用户群,使 Kubernetes 仍然保持3个月一个版本的高频发布节奏。高频的版本发布带来了更多的新功能落地和 bug 及时修复,但是线上环境业务长期运行,任何变更出错都可能带来巨大的经济损失,升级对企业来说相对吃力,紧跟社区更是几乎不可能,因此高频发布和稳定生产之间的矛盾需要容器团队去衡量和取舍。 vivo 互联网团队建设大规模 Kubernetes 集群以来,部分集群较长时间一直使用 v1.10 版本,但是由于业务容器化比例越来越高,对大规模集群稳定性、应用发布的多样性等诉求日益攀升,集群升级迫在眉睫。集群升级后将解决如下问题: 高版本集群在大规模场景做了优化,升级可以解决一系列性能瓶颈问题。 高版本集群才能支持 OpenKruise 等 CNCF 项目,升级可以解决版本依赖问题。 高版本集群增加的新特性能够提高集群资源利用率,降低服务器成本同时提高集群效率。 公司内部维护多个不同版本集群,升级后减少集群版本碎片化,进一步降低运维成本。 这篇文章将会从0到1的介绍 vivo 互联网团队支撑在线业务的集群如何在不影响原有业务正常运行的情况下从 v1.10 版本升级到 v1.17 版本。之所以升级到 v1.17 而不是更高的 v1.18 以上版本, 是因为在 v1.18 版本引入的代码变动 [1] 会导致 extensions/v1beta1 等高级资源类型无法继续运行(这部分代码在 v1.18 版本删除)。 二、无损升级难点 容器集群搭建通常有二进制 systemd 部署和核心组件静态 Pod 容器化部署两种方式,集群 API 服务多副本对外负载均衡。两种部署方式在升级时没有太大区别,二进制部署更贴合早期集群,因此本文将对二进制方式部署的集群升级做分享。 对二进制方式部署的集群,集群组件升级主要是二进制的替换、配置文件的更新和服务的重启;从生产环境 SLO 要求来看,升级过程务必不能因为集群组件自身逻辑变化导致业务重启。因此升级的难点集中在下面几点: 首先,当前内部集群运行版本较低,但是运行容器数量却很多,其中部分仍然是单副本运行,为了不影响业务运行,需要尽可能避免容器重启,这无疑是升级中最大的难点,而在 v1.10 版本和 v1.17 版本之间,kubelet 关于容器 Hash 值计算方式发生了变化,也就是说一旦升级必然会触发 kubelet 重新启动容器。 其次,社区推荐的方式是基于偏差策略 [2] 的升级以保证高可用集群升级同时不会因为 API resources 版本差异导致 kube-apiserve 和 kubelet 等组件出现兼容性错误,这就要求每次升级组件版本不能有2个 Final Release 以上的偏差,比如直接从 v1.11 升级至 v1.13是不推荐的。 再次,升级过程中由于新特性的引入,API 兼容性可能引发旧版本集群的配置不生效,为整个集群埋下稳定性隐患。这便要求在升级前尽可能的熟悉升级版本间的 ChangeLog,排查出可能带来潜在隐患的新特性。 三、无损升级方案 针对前述的难点,本节将逐个提出针对性解决方案,同时也会介绍升级后遇到的高版本 bug 和解决方法。希望关于升级前期兼容性筛查和升级过程中排查的问题能够给读者带来启发。 3.1 升级方式 在软件领域,主流的应用升级方式有两种,分别是原地升级和替换升级。目前这两种升级方式在业内互联网大厂均有采用,具体方案选择与集群上业务有很大关系。 替换升级 1)Kubernetes 替换升级是先准备一个高版本集群,对低版本集群通过逐个节点排干、删除最后加入新集群的方式将低版本集群内节点逐步轮换升级到新版本。 2)替换升级的优点是原子性更强,逐步升级各个节点,升级过程不存在中间态,对业务安全更有保障;缺点是集群升级工作量较大,排干操作对pod重启敏感度高的应用、有状态应用、单副本应用等都不友好。 原地升级 1)Kubernetes 原地升级是对节点上服务如 kube-controller-manager、 kubelet 等组件按照一定顺序批量更新,从节点角色维度批量管理组件版本。 2)原地升级的优点是自动化操作便捷,并且通过适当的修改能够很好的保证容器的生命周期连续性;缺点是集群升级中组件升级顺序很重要,升级中存在中间态,并且一个组件重启失败可能影响后续其他组件升级,原子性差。 vivo 容器集群上运行的部分业务对重启容忍度较低,尽可能避免容器重启是升级工作的第一要务。当解决好升级版本带来的容器重启后,结合业务容器化程度和业务类型不同,因地制宜的选择升级方式即可。二进制部署集群建议选择原地升级的方式,具有时间短,操作简捷,单副本业务不会被升级影响的好处。 3.2 跨版本升级 由于Kubernetes 本身是基于 API 的微服务架构,Kuberntes 内部架构也是通过 API 的调用和对资源对象的 List-Watch 来协同资源状态,因此社区开发者在设计 API 时遵循向上或向下兼容的原则。这个兼容性规则也是遵循社区的偏差策略 [2],即 API groups 弃用、启用时,对于 Alpha 版本会立即生效,对于 Beta 版本将会继续支持3个版本,超过对应版本将导致 API resource version 不兼容。例如 kubernetes 在 v1.16 对 Deployment 等资源的 extensions/v1beta1 版本执行了弃用,在v1.18 版本从代码级别执行了删除,当跨3个版本以上升级时会导致相关资源无法被识别,相应的增删改查操作都无法执行。 如果按照官方建议的升级策略,从 v1.10 升级到 v1.17 需要经过至少 7 次升级,这对于业务场景复杂的生产环境来说运维复杂度高,业务风险大。 对于类似的 API breaking change 并不是每个版本都会存在,社区建议的偏差策略是最安全的升级策略,经过细致的 Change Log 梳理和充分的跨版本测试,我们确认这几个版本之间不能存在影响业务运行和集群管理操作的 API 兼容性问题,对于 API 类型的废弃,可以通过配置 apiserver 中相应参数来启动继续使用,保证环境业务继续正常运行。 3.3 避免容器重启 在初步验证升级方案时发现大量容器都被重建,重启原因从升级后 kubelet 组件日志看到是 "Container definition changed"。结合源码报错位于 pkg/kubelet/kuberuntime_manager.go 文件 computePodActions 方法,该方法用来计算 pod 的 spec 哈希值是否发生变化,如果变化则返回 true,告知 kubelet syncPod 方法触发 pod 内容器重建或者 pod 重建。 kubelet 容器 Hash 计算; func (m *kubeGenericRuntimeManager) computePodActions(pod *v1.Pod, podStatus *kubecontainer.PodStatus) podActions { restart := shouldRestartOnFailure(pod) if _, _, changed := containerChanged(&container, containerStatus); changed { message = fmt.Sprintf("Container %s definition changed", container.Name) // 如果 container spec 发生变化,将会强制重启 container(将 restart 标志位设置为 true) restart = true } ... if restart { message = fmt.Sprintf("%s, will be restarted", message) // 需要重启的 container 加入到重启列表 changes.ContainersToStart = append(changes.ContainersToStart, idx) } } func containerChanged(container *v1.Container, containerStatus *kubecontainer.ContainerStatus) (uint64, uint64, bool) { // 计算 container spec 的 Hash 值 expectedHash := kubecontainer.HashContainer(container) return expectedHash, containerStatus.Hash, containerStatus.Hash != expectedHash } 相对于 v1.10 版本,v1.17 版本在计算容器 Hash 时使用的是 container 结构 json 序列化后的数据,而不是 v1.10 版本使用 container struct 的结构数据。而且高版本 kubelet 中对容器的结构也增加了新的属性,通过 go-spew 库计算出结果自然不一致,进一步向上传递返回值使得 syncPod 方法触发容器重建。 那是否可以通过修改 go-spew 对 container struct 的数据结构剔除新增的字段呢? 答案是肯定的,但是却不是优雅的方式,因为这样对核心代码逻辑侵入较为严重,以后每个版本的升级都需要定制代码,并且新增的字段越来越多,维护复杂度也会越来越高。换个角度,如果在升级过渡期间将属于旧版本集群 kubelet 创建的 Pod 跳过该检查,则可以避免容器重启。 和圈内同事交流后发现类似思路在社区已有实现,本地创建一个记录旧集群版本信息和启动时间的配置文件,kubelet 代码中维护一个 cache 读取配置文件,在每个 syncPod 周期中,当 kubelet 发现自身 version 高于 cache 中记录的 oldVersion, 并且容器启动时间早于当前 kubelet 启动时间,则会跳过容器 Hash 值计算。升级后的集群内运行定时任务探测 Pod 的 containerSpec 是否与高版本计算方式计算得到 Hash 结果全部一致,如果是则可以删除掉本地配置文件,syncPod 逻辑恢复到与社区完全一致。 具体方案参考这种实现的好处是对原生 kubelet 代码侵入小,没有改变核心代码逻辑,而且未来如果还需要升级高版本也可以复用该代码。如果集群内所有 Pod 都是当前版本 kubelet 创建,则会恢复到社区自身的逻辑。 3.4 Pod 非预期驱逐问题 Kubernetes 虽然迭代了十几个版本,但是每个迭代社区活跃度仍然很高,保持着每个版本大约30个关于拓展性增强和稳定性提升的新特性。选择升级很大一方面原因是引入很多社区开发的新特性来丰富集群的功能与提升集群稳定性。新特性开发也是遵循偏差策略,跨大版本升级很可能导致在部分配置未加载的情况下启用新特性,这就给集群带来稳定性风险,因此需要梳理影响 Pod 生命周期的一些特性,尤其关注控制器相关的功能。 这里注意到在 v1.13 版本引入的 TaintBasedEvictions 特性用于更细粒度的管理 Pod 的驱逐条件。在 v1.13基于条件版本之前,驱逐是基于 NodeController 的统一时间驱逐,节点 NotReady 超过默认5分钟后,节点上的 Pod 才会被驱逐;在 v1.16 默认开启 TaintBasedEvictions 后,节点 NotReady 的驱逐将会根据每个 Pod 自身配置的 TolerationSeconds 来差异化的处理。 旧版本集群创建的 Pod 默认没有设置 TolerationSeconds,一旦升级完毕 TaintBasedEvictions 被开启,节点变成 NotReady 后 5 秒就会驱逐节点上的 Pod。对于短暂的网络波动、kubelet 重启等情况都会影响集群中业务的稳定性。 TaintBasedEvictions 对应的控制器是按照 pod 定义中的 tolerationSeconds 决定 Pod 的驱逐时间,也就是说只要正确设置 Pod 中的 tolerationSeconds 就可以避免出现 Pod 的非预期驱逐。 在v1.16 版本社区默认开启的 DefaultTolerationSeconds 准入控制器基于 k8s-apiserver 输入参数 default-not-ready-toleration-seconds 和 default-unreachable-toleration-seconds 为 Pod 设置默认的容忍度,以容忍 notready:NoExecute 和 unreachable:NoExecute 污点。 新建 Pod 在请求发送后会经过 DefaultTolerationSeconds 准入控制器给 pod 加上默认的 tolerations。但是这个逻辑如何对集群中已经创建的 Pod 生效呢?查看该准入控制器发现除了支持 create 操作,update 操作也会更新 pod 定义触发 DefaultTolerationSeconds 插件去设置 tolerations。因此我们通过给集群中已经运行的 Pod 打 label 就可以达成目的。 tolerations: - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 3.5 Pod MatchNodeSelector 为了判断升级时 Pod 是否发生非预期的驱逐以及是否存在 Pod 内容器批量重启,有脚本去实时同步节点上非Running状态的Pod和发生重启的容器。 在升级过程中,突然多出来数十个 pod 被标记为 MatchNodeSelector 状态,查看该节点上业务容器确实停止。kubelet 日志中看到如下错误日志; predicate.go:132] Predicate failed on Pod: nginx-7dd9db975d-j578s_default(e3b79017-0b15-11ec-9cd4-000c29c4fa15), for reason: Predicate MatchNodeSelector failed kubelet_pods.go:1125] Killing unwanted pod "nginx-7dd9db975d-j578s" 经分析,Pod 变成 MatchNodeSelector 状态是因为 kubelet 重启时对节点上 Pod 做准入检查时无法找到节点满足要求的节点标签,pod 状态就会被设置为 Failed 状态,而 Reason 被设置为 MatchNodeSelector。在 kubectl 命令获取时,printer 做了相应转换直接显示了Reason,因此我们看到 Pod 状态是 MatchNodeSelector。通过给节点加上标签,可以让 Pod 重新调度回来,然后删除掉 MatchNodeSelector 状态的 Pod 即可。 建议在升级前写脚本检查节点上 pod 定义中使用的 NodeSelector 属性节点是否都有对应的 Label。 3.6 无法访问 kube-apiserver 预发环境升级后的集群运行在 v1.17 版本后,突然有节点变成 NotReady 状态告警,分析后通过重启 kubelet 节点恢复正常。继续分析出错原因发现 kubelet 日志中出现了大量 use of closed network connection 报错。在社区搜索相关 issue 发现有类似的问题,其中有开发者描述了问题的起因和解决办法,并且在 v1.18 已经合入了代码。 问题的起因是 kubelet 默认连接是 HTTP/2.0 长连接,在构建 client 到 server的连接时使用的 golang net/http2 包存在 bug,在 http 连接池中仍然能获取到 broken 的连接,也就导致 kubelet 无法正常与 kube-apiserver 通信。 golang社区通过增加 http2 连接健康检查规避这个问题,但是这个 fix 仍然存在 bug ,社区在 golang v1.15.11 版本彻底修复。我们内部通过 backport 到 v1.17 分支,并使用 golang 1.15.15 版本编译二进制解决了此问题。 3.7 TCP 连接数问题 在预发布环境测试运行期间,偶然发现集群每个节点 kubelet 都有近10个长连接与 kube-apiserver 通信,这与我们认知的 kubelet 会复用连接与 kube-apiserver 通信明显不符,查看 v1.10 版本环境也确实只有1个长连接。这种 TCP 连接数增加情况无疑会对 LB 造成了压力,随着节点增多,一旦 LB 被拖垮,kubelet 无法上报心跳,节点会变成 NotReady,紧接着将会有大量 Pod 被驱逐,后果是灾难性的。因此除去对 LB 本身参数调优外,还需要定位清楚kubelet 到 kube-apiserver 连接数增加的原因。 在本地搭建的 v1.17.1 版本 kubeadm 集群 kubelet 到 kube-apiserver 也仅有1个长连接,说明这个问题是在 v1.17.1 到升级目标版本之间引入的,排查后(问题)发现增加了判断逻辑导致 kubelet 获取 client 时不再从 cache 中获取缓存的长连接。transport 的主要功能其实就是缓存了长连接,用于大量 http 请求场景下的连接复用,减少发送请求时 TCP(TLS) 连接建立的时间损耗。在该 PR 中对 transport 自定义 RoundTripper 的接口,一旦 tlsConfig 对象中有 Dial 或者 Proxy 属性,则不使用 cache 中的连接而新建连接。 // client-go 从 cache 获取复用连接逻辑 func tlsConfigKey(c *Config) (tlsCacheKey, bool, error) { ... if c.TLS.GetCert != nil || c.Dial != nil || c.Proxy != nil { // cannot determine equality for functions return tlsCacheKey{}, false, nil } ... } func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) { key, canCache, err := tlsConfigKey(config) ... if canCache { // Ensure we only create a single transport for the given TLS options c.mu.Lock() defer c.mu.Unlock() // See if we already have a custom transport for this config if t, ok := c.transports[key]; ok { return t, nil } } ... } // kubelet 组件构建 client 逻辑 func buildKubeletClientConfig(ctx context.Context, s *options.KubeletServer, nodeName types.NodeName) (*restclient.Config, func(), error) { ... kubeClientConfigOverrides(s, clientConfig) closeAllConns, err := updateDialer(clientConfig) ... return clientConfig, closeAllConns, nil } // 为 clientConfig 设置 Dial属性,因此 kubelet 构建 clinet 时会新建 transport func updateDialer(clientConfig *restclient.Config) (func(), error) { if clientConfig.Transport != nil || clientConfig.Dial != nil { return nil, fmt.Errorf("there is already a transport or dialer configured") } d := connrotation.NewDialer((&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext) clientConfig.Dial = d.DialContext return d.CloseAll, nil 在这里构建 closeAllConns 对象来关闭已经处于 Dead 但是尚未 Close 的连接,但是上一个问题通过升级 golang 版本解决了这个问题,因此我们在本地代码分支回退了该修改中的部分代码解决了 TCP 连接数增加的问题。 最近追踪社区发现已经合并了解决方案 ,通过重构 client-go 的接口实现对自定义 RESTClient 的 TCP 连接复用。 四、无损升级操作 跨版本升级最大的风险是升级前后对象定义不一致,可能导致升级后的组件无法解析保存在 ETCD 数据库中的对象;也可能是升级存在中间态,kubelet 还未升级而控制平面组件升级,存在上报状态异常,最坏的情况是节点上 Pod 被驱逐。这些都是升级前需要考虑并通过测试验证的。 经过反复测试,上述问题在 v1.10 到 v1.17 之间除了部分废弃的 API Resources 通过增加 kube-apiserver 配置方式其他情况暂时不存在。为了保证升级时及时能处理未覆盖到的特殊情况,强烈建议升级前备份 ETCD 数据库,并在升级期间停止控制器和调度器,避免非预期的控制逻辑发生(实际上这里应该是停止 controller manager 中的部分控制器,不过需要修改代码编译临时 controller manager ,增加了升级流程步骤和管理复杂度,因此直接停掉了全局控制器)。 除却以上代码变动和升级流程注意事项,在替换二进制升级前,就剩下比对新老版本服务的配置项的区别以保证服务成功启动运行。对比后发现,kubelet 组件启动时不再支持 --allow-privileged 参数,需要删除。值得说明的是,删除不代表高版本不再支持节点上运行特权容器,在 v1.15 以后通过 Pod Security Policy 资源对象来定义一组 pod 访问的安全特征,更细粒度的做安全管控。 基于上面讨论的无损升级代码侧的修改编译二进制,再对集群组件配置文件中各个配置项修改后,就可以着手线上升级。整个升级步骤为: 备份集群(二进制,配置文件,ETCD数据库等); 灰度升级部分节点,验证二进制和配置文件正确性 提前分发升级的二进制文件; 停止控制器、调度器和告警; 更新控制平面服务配置文件,升级组件; 更新计算节点服务配置文件,升级组件; 为节点打 Label 触发 pod 增加 tolerations 属性; 打开控制器和调度器,启用告警; 集群业务点检,确认集群正常。 升级过程中建议节点并发数不要太高,因为大量节点 kubelet 同时重启上报信息,对 kube-apiserver 前面使用的 LB 带来冲击,特别情况下可能节点心跳上报失败,节点状态会在 NotReady 与 Ready 状态间跳动。 五、总结 集群升级是困扰容器团队比较长时间的事,在经过一系列调研和反复测试,解决了上面提到的数个关键问题后,成功将集群从 v1.10 升级到 v1.17 版本,1000 个节点的集群分批执行升级操作,大概花费 10 分钟,后续在完成平台接口改造后将会再次升级到更高版本。 集群版本升级提高了集群的稳定性、增加了集群的扩展性,同时还丰富了集群的能力,升级后的集群也能够更好的兼容 CNCF 项目。 如开篇所述,按照偏差策略频繁对大规模集群升级可能不太现实,因此跨版本升级虽然风险较大,但是也是业界广泛采用的方式。在 2021 年中国 KubeCon 大会上,阿里巴巴也有关于零停机跨版本升级 Kubernetes 集群的分享,主要是关于应用迁移、流量切换等升级关键点的介绍,升级的准备工作和升级过程相对复杂。相对于阿里巴巴的集群跨版本替换升级方案,原地升级的方式需要在源码上做少量修改,但是升级过程会更简单,运维自动化程度更高。 由于集群版本具有很大的可选择性,本文所述的升级并不一定广泛适用,笔者更希望给读者提供生产集群在跨版本升级时的思路和风险点。升级过程短暂,但是升级前的准备和调研工作是费时费力的,需要对不同版本 Kubernetes 特性和源码深入探索,同时对 Kubernetes 的 API 兼容性策略和发布策略拥有完整认知,这样便能在升级前做出充分的测试,也能更从容面对升级过程中突发情况。 六、参考链接 [1]https://github.com [2] https://kubernetes.io/version-skew-policy [3] 具体方案参考:https://github.comstart [4] 类似的问题: https://github.com/kubernetes [5] https://github.com/golang/34978 [6] https://github.com/kubernetes/100376 [7] https://github.com/kubernetes/95427 [8] https://github.com/kubernetes/105490 作者:vivo互联网服务器团队-Shu Yingya

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

Vite + React 组件开发实践

简介:毫不夸张的说,Vite 给前端带来的绝对是一次革命性的变化。或者也可以说是 Vite 背后整合的 esbuild 、 Browser es modules、HMR、Pre-Bundling 等这些社区中关于 JS 编译发展的先进工具和思路,在 Vite 这样的整合推动下,给前端开发带来了革命性变化。 作者 | 风水 来源 | 阿里技术公众号 去年发表的《一个好的组件应该是什么样的?》 一文介绍了借助 TypeScript AST 语法树解析,对 React 组件 Props 类型定义及注释提取,自动生成组件对应 截图、用法、参数说明、README、Demo 等。在社区中取得了比较好的反响,同时应用在团队中也取得了较为不错的结果,现在内部组件系统中已经累计使用该方案沉淀 1000+ 的 React 组件。 之前我们是借助了 webpack + TypeScript 做了一套用于开发 React 组件的脚手架套件,当开发者要组件开发时,即可直接使用脚手架初始化对应项目结构进行开发。 虽然主路径上确实解决了组件开发中所遇到的组件无图无真相、组件参数文档缺失、组件用法文档缺失、组件 Demo 缺失、组件无法索引、组件产物不规范等内部组件管理和沉淀上的问题,但 Webpack 的方案始终还是会让组件开发多一层编译,当一个组件库沉淀超过 300+ 时,引入依赖不断增长,还是会带来组件编译上的负荷导致开发者开发体验下降。 一 Vite 带来的曙光 Vite 给前端带来的绝对是一次革命性的变化,这么说毫不夸张。 或许应该说是 Vite 背后整合的 esbuild 、 Browser es modules、HMR、Pre-Bundling 等这些社区中关于 JS 编译发展的先进工具和思路,在 Vite 这样的整合推动下,给前端开发带来了革命性变化。 我很早就说过,任何一个框架或者库的出现最有价值的一定不是它的代码本身,而是这些代码背后所带来的新思路、新启发。所以我在写文章的时候,也很注重能把我思考最后执行的整个过程讲清楚。 Vite 为什么快,主要是 esbuild 进行 pre-bundles dependencies + 浏览器 native ESM 动态编译,这里我不做过多赘述,详细参考:Vite: The Problems 在这个思路的背景下,回到我们组件开发的场景再看会发现以下几个问题高度吻合: 组件库开发,实际上不需要编译全部组件。 组件开发,编译预览页面主要给开发者使用,浏览器兼容可控。 HMR(热更新)能力在 Vite 加持下更加显得立竿见影,是以往组件开发和调试花费时间最多的地方。 Vite 中一切源码模块动态编译,也就是 TypeScript 类型定义和 JS 注释也可以做到动态编译,大大缩小编译范围。 那么,以往像 StoryBook 和之前我们用于提取 tsx 组件类型定义的思路将可以做一个比较大的改变。 之前为了获取组件入参的类型数据会在 Wwebpack 层面做插件用于动态分析 export 的 tsx 组件,在该组件下动态加入一段 __docgenInfo 的静态属性变量,将从 AST 分析得到的类型数据和注释信息注入进组件 JS Bundle,从而进一步处理为动态参数设置: TypeScript 对组件 Props 的定义 分析注入到 JS Bundle 中的内容 分析转换后实现的参数交互设置 所以对于组件来说,实际上获取这一份类型定义的元数据对于组件本身来说是冗余的,不论这个组件中的这部分元数据有没有被用到,都会在 Webpack 编译过程中解析提取并注入到组件 Bundle 中,这显然是很低效的。 在 Vite 的思路中,完全可以在使用到组件元数据时,再获取其元数据信息,比如加载一个 React 组件为: import ReactComponent from './component1.tsx' 那么加载其元数据即: import ComponentTypeInfo from './component1.tsx.type.json'; // or const ComponentTypeInfoPromise = import('./component1.tsx.type.json'); 通过 Vite 中 Rollup 的插件能力加载 .type.json 文件类型,从而做到对应组件元数据的解析。同时借助 Rollup 本身对于编译依赖收集和 HMR 的能力,做到组件类型变化的热更新。 二 设计思路 以上是看到 Vite 的模块加载思路,得到的一些灵感和启发,从而做出的一个初步设想。 但如果真的要做这样一个基于 Vite 的 React 、 Rax 组件开发套件,除了组件入参元数据的获取以外,当然还有其他需要解决的问题,首当其冲的就是对于 .md 的文件解析。 1 组件 Usage 参照 dumi 及 Icework 所提供的组件开发思路,组件 Usage 完全可以以 Markdown 写文档的形式写到任何一个 .md 文件中,由编译器动态解析其中关于 jsx、tsx、css、scss、less 的代码区块,并且把它当做一段可执行的 script 编译后,运行在页面中。 这样既是在写文档,又可以运行调试组件不同入参下组件表现情况,组件有多少中Case,可以写在不同的区块中交由用户自己选择查看,这个设计思路真是让人拍案叫绝! 最后,如果能结合上述提到 Vite 的 esbuild 动态加载和 HMR 能力,那么整个组件开发体验将会再一次得到质的飞跃。 所以针对 Markdown 文件需要做一个 Vite 插件来执行对 .md 的文件解析和加载,预期要实现的能力如下: import { content, modules } from "./component1/README.md"; // content README.md 的原文内容 // modules 通过解析获得的`jsx`,`tsx`,`css`,`scss`,`less` 运行模块 预期设想效果,请点击放大查看: 2 组件 Runtime 一个常规的组件库目录应该是什么样的?不论是在一个单独的组件仓库,还是在一个已有的业务项目中,其实组件的目录结构大同小异,大致如下: components ├── component1 │ ├── README.md │ ├── index.scss │ └── index.tsx ├── component2 │ ├── README.md │ ├── index.scss │ └── index.tsx 在我们的设想中你可以在任意一个项目中启动组件开发模式,在运行 vite-comp 之后就可以看到一个专门针对组件开发的界面,在上面已经帮你解析并渲染出来了在 README.md 中编写的组件 Usage,以及在 index.tsx 定义的 interface,只需要访问不同的文件路径,即可查看对应组件的表现形态。 同时,最后可以帮你可以将这个界面上的全部内容编译打包,截图发布到 NPM 上,别人看到这个组件将会清晰看到其组件入参,用法,截图等,甚至可以打开 Demo 地址,修改组件参数来查看组件不同状态下的表现形态。 如果要实现这样的效果,则需要一套组件运行的 Runtime 进行支持,这样才可以协调 React 组件、README.md、TypeScript 类型定义串联成我们所需要的组件调试+文档一体的组件开发页面。 在这样的 Runtime 中,同样需要借助 Vite 的模块解析能力,将其 URL 为*//(README|*).html 的请求,转换为一段可访问的组件 Runtime Html 返回给浏览器,从而让浏览器运行真正的组件开发页面。 http://localhost:7000/components/component1/README.html -> /components/component1/README.html -> /components/component1/README.md -> Runtime Html 3 组件 Props Interface 正如我上述内容中讲到的,如果利用 Vite 添加一个对 tsx 的组件 props interface 类型解析的能力,也可以做成独立插件用于解析 .tsx.type.json 结尾的文件类型,通过 import 这种类型的文件,从而让编译器动态解析其 tsx 文件中所定义的 TypeScript 类型,并作为模块返回给前端消费。 其加载过程就可以当做是一个虚拟的模块,可以理解为你可以通过直接 import 一个虚拟的文件地址,获取到对应的 React 组件元信息: // React Component import Component from './component1.tsx'; // React Component Props Interface import ComponentTypeInfo from './component1.tsx.type.json'; // or const ComponentTypeInfoPromise = import('./component1.tsx.type.json'); 由于这种解析能力并不是借助于 esbuild 进行,所以在转换性能上无法和组件主流程编译同步进行。 在请求到该文件类型时,需要考虑在 Vite 的 Serve 模式下,新开线程进行这部分内容编译,由于整个过程是异步行为,不会影响组件主流程渲染进度。当请求返回响应后,再用于渲染组件 Props 定义及侧边栏面板部分。 在热更新过程中,同样需要考虑到 tsx 文件修改范围是否涉及到 TypeScript 类型的更改,如果发现修改导致类型变化时,再触发 HMR 事件进行模块更新。 三 组件 Build 以上都是在讨论组件在 Vite 的 Serve 态(也就是开发态)下的情况,我们上文中大量借助 Vite 利用浏览器 es module 的加载能力,从而做的一些开发态的动态加载能力的扩展。 但是 Vite 在组件最终 Build 过程中是没有 Server 服务启动,当然也不会有浏览器动态加载,所以为了让别人也可以看到我们开发的组件,能够体验我们开发时调试组件的样子,就需要考虑为该组件编译产出一份可以被浏览器运行的 html。 所以在 Vite 插件开发过程中,是需要考虑在 Build 状态下的编译路径的,如果是在 Build 状态下,Vite 将使用 Rollup 的编译能力,那么就需要考虑手动提供所有组件的 rollup.input(entries)。 在插件编写过程中,一定需要遵循 Rollup 所提供的插件加载生命周期,才能保证 Build 过程和 Serve 过程的模块加载逻辑和编译逻辑保持一致。 我一开始在实现的过程中,就是没有了解透彻 Vite 和 Rollup 的关系,在模块解析过程中依赖了大量 Vite 的 Server 提供的服务端中间件能力。导致在考虑到 Build 态时,才意识到其中的问题,最后几乎重新写了之前的加载逻辑。 四 总结 我姑且把这个方案(套件)称之为 vite-comp,其大致的构成就是由 Vite + 3 Vite Pugins 构成,每个插件相互不耦合,相互职责也不相同,也就是说你可以拿到任意一个 Vite 插件去做别的用途,后续会考虑单独开源,分别是: Markdown,用于解析 .md 文件,加载后可获取原文及 jsx、tsx 等可运行区块。 TypeScript Interface,用于解析 .tsx 文件中对于 export 组件的 props 类型定义。 Vite Comp Runtime,用于运行组件开发态,编译最终组件文档。 结合 Vite,已经实现了 Vite 模式下的 React、Rax 组件开发,它相比于之前使用 Webpack 做的组件开发,已经体现出了以下几个大优势: 无惧大型组件库,即使有 2000 个组件在同一个项目中,启动依旧是 <1000ms。 高效的组件元数据加载流,项目一切依赖编译按需进行。 毫秒级热更新响应,借助 esbuild 几乎是按下保存的一瞬间,就可以看到改动效果。 预览体验: 启动 Markdown 组件文档毫秒级响应 TypeScript 类型识别 Vite 现在还是只是刚刚起步,这种全新的编译模式,已经给我带来了非常多的开发态收益,结合 Vite 的玩法未来一定还会层出不穷,比如 Midway + lambda + Vite 的前端一体化方案也是看得让人拍案叫绝,在这个欣欣向荣的前端大时代,相信不同前端产物都会和 Vite 结合出下一段传奇故事。 我是一个热爱生活的前端工程师!Yooh! 相关链接 https://vitejs.dev/guide/why.html#the-problems https://d.umijs.org/ https://ice.work/ 前端开发技术图谱 6 大知识点,14 个课程,680 个课时,将前端开发知识和实战经验融入图谱,包含 HTML 、CSS、JavaScript 、jQuery 、Vue 、React 、Angular 、NodeJS 等前端开发必备技能,帮你迅速提升。 本文为阿里云原创内容,未经允许不得转载。

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

Pgbouncer最佳实践:系列三

作者:王志斌,曾获得中国PostgreSQL数据库管理工程师(PGCE),是PostgreSQL官方认证讲师,盘古云课堂特邀金牌讲师。 PgBouncer具有三种可用的池模式:事务池,会话池和语句池: 事务连接池 数据库客户端很少在不间断的情况下执行连续的事务。而是通常在事务之间执行非数据库工作。这意味着服务器连接在等待新工作到达时会花费大量时间空闲。 事务池模式试图减少服务器连接的空闲时间,如下所示: 池程序在开始事务时将服务器连接分配给客户端。 客户端的事务完成后,池程序将释放连接分配。 注意事项: 如果客户端运行多个事务,则每个事务可以在不同的服务器连接上执行。 单个服务器连接可以在其生命周期内运行由不同客户端发出的事务。 图 6 事务连接池 与服务器所允许的连接相比,允许活动客户端的数量要多得多。尽管取决于给定的工作负载,但经常会看到10倍或更多的活动客户端连接与服务器连接比率。 这确实带来了一个重要的警告:客户端不再期望对数据库会话状态所做的更改在同一客户端进行的连续事务中继续存在,因为这些事务可能在不同的服务器连接上运行。此外,如果客户端进行会话状态更改,它们可能并且很可能会影响其他客户端。 以下是一些使用上面的事务池示例: 如果客户端1在T1中的第一个服务器连接上将会话设置为只读,而客户端2的T3是写事务,则T3将失败,因为它在现在的只读服务器连接上运行。 如果客户端1运行PREPARE a1 AS ...在T1上运行EXECUTE a1 ...,在T2上,则T2将失败,因为预编译语句对于运行T1的服务器连接是本地的。 如果客户端2在T3中创建了一个临时表并尝试在T4中使用它,则T4将失败,因为该临时表对于运行T3的服务器连接是本地的。 有关使用事务池时不支持的会话状态功能和操作的完整列表,请参见PgBouncer的列表 会话连接池 分配给客户端的服务器连接在客户端连接的整个生命周期内持续。这看起来好像根本不使用连接池一样,但是有一个重要的区别:当分配的客户端断开连接时,服务器连接不会被破坏。当客户端断开连接时,池管理器将: 清除客户端所做的任何会话状态更改。 将服务器连接返回到池中,以供其他客户端使用。 图 7 会话连接池 语句连接池 在此,服务器连接分配仅在单个语句的持续时间内持续。这具有与事务池模式相同的会话状态限制,同时还破坏了事务语义。 图 8 语句连接池 这使得所有客户端连接的行为就像在“自动提交”模式下一样。如果客户端尝试开始多语句事务,则合并程序将返回错误。尽管这是 表 3 连接池模式对比 从上述对比情况来看,在连接池的选择上,需要依据业务环境特点来进行选择,默认情况下推荐使用事务连接池,它兼顾了执行事务的特性,尤其多语句的支持,并且不会像会话连接池那样,尝尝处于等待状态。当然事务模式并不支持预编译语句。而根据具体业务场景的特殊需要,有些时候需要客户端与服务器端保持连接,或者支持预编译语句,这样只能选择会话池模式。还有一些特例情况,某些业务场景只是单语句执行,那么语句池模式可能更适合。因此对比这三种模式,可以发现从对客户端操作的支持程度来讲,会话池支持度最高,其次是事务池,最后是语句池模式。但是从支持的连接数来讲,可能刚好是相反的顺序。 表 4 SQL特性对照表 上表为会话连接池和事务连接池的SQL特性对比情况,可以通过对比具体业务场景与SQL特性的符合度,来对连接池模式进行选型。 下面列举了一些示例场景: 有些只运行快速查询,因此在没有事务的情况下可以共享一个会话来处理上百个并发查询。 一些角色成员对于会话级并发是安全的,并且总是使用事务。因此,他们可以安全地共享数百个并发事务的多个会话。 有些角色过于复杂,无法与其他人共享会话。因此,您对它们使用会话池模式可以避免当所有“插槽”都已占用时连接错误。 不要使用它代替HAProxy或其他负载均衡器。尽管pgbouncer具有一些可配置的功能来解决负载均衡器要解决的问题,例如dns_max_ttl,并且可以为其设置DNS配置,但是大多数产品环境都使用HAProxy或其他用于HA的负载均衡器。这是因为HAProxy确实擅长以循环方式在服务器之间实现负载平衡,而不是pgbouncer。尽管pgbouncer对于postgres连接池更好,但最好使用一个小型守护程序来完美地执行一项任务,而不是使用较大的守护程序来完成两项任务,那样效果更糟。 在对于连接数的建议值来讲,上文也给出了一个大致的结果,就是一般情况下设置为CPU核数的3-4倍左右,当然这个不是绝对值,应该是在与业务场景类似的硬件环境中充分进行测试后,才能够得出具体的数值。 还有一点需要注意的是连接Pgbouncer的连接方式,网络连接和unix socket连接方式,较网络连接,unix socket方式可能更加节省网络通信的开销,因此如果pgbouncer和数据库在一台机器部署,可以优选该方式;如果处于不同服务器上,则选择网络连接。 了解更多PostgreSQL热点资讯、新闻动态、精彩活动,请访问中国PostgreSQL官方网站 解决更多PostgreSQL相关知识、技术、工作问题,请访问中国PostgreSQL官方问答社区 下载更多PostgreSQL相关资料、工具、插件问题,请访问中国PostgreSQL官方下载网站

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

Phoenix索引构建最佳实践

用户福利 阿里云发布业界首款云原生多模数据库Lindorm,新用户可享9.9元/3个月优惠,技术交流钉钉群:35977898,更多内容请参考链接 背景 Phoenix的索引构建有两类方法: 同步构建,直接通过sqlline.py create index构建 在云HBase Phoenix 5.x之后,同步构建可以通过轻客户端或重客户端来构建。 异步构建,先create index ... async, 然后通过MR提交build索引job。 因此我们有三种方式构建索引:轻客户端、重客户端、MR异步构建,我们依次介绍下各种方案的优缺点、适用场景和使用方法。 同步构建-轻客户端 适用与数据量比较小,一般构建耗时在10分钟以内。使用方式: 直接使用轻客户端, sqlline-thin.py , create index 即可。 如果数据量较大,我们很可能会遇到索引build超时,我们释放调整Phoenix hbase.rpc.timeout、hbase.client.scanner.timeout.period、phoenix.query.timeoutMs 配置,重启Queryserver生效。 优点: 简单,不占用客户端资源,整个build过程是在服务端完成的。 缺点: 调整参数需要重启queryserver生效,重启过程会导致线上服务临时中断 调整参数会对线上服务造成影响如果有异常SQL导致的大请求会导致服务端负载高,调整了RPC超时时间, 一旦遇到这种请求,无法及时中断,可能对线上业务产生的影响。 无法控制并发,可能因为索引构建打爆服务器 同步构建-重客户端 重客户端适用于中小规模的数据构建,一般索引构建时间在10小时以内。 使用方式: 下载重客户端工具 部署在用户VPC下,机器配置大于等于4c8g;重客户端构建索引的过程中流量会经过这个节点,如果需要更快的索引构建,可以升级节点配置。 调整配置 bin/hbase-site.xml后,使用bin/sqline.py 集群地址 hbase.zookeeper.quorumzk 连接地址,注意使用VPC链接地址。 并发数 phoenix.query.threadPoolSize并发数,越大对目标集群读写压力,可以从1开始逐步增加 超时配置 phoenix.query.keepAliveMs hbase.rpc.timeout hbase.client.scanner.timeout.period phoenix.query.timeoutMs超时时间单位是ms, 可以按需调整。 for ex: <property> <name>hbase.zookeeper.quorum</name> <value>master1-1,master2-1,master3-1:2181</value> </property> <property> <name>phoenix.query.threadPoolSize</name> <value>1</value> </property> <property> <name>phoenix.query.keepAliveMs</name> <value>60000000</value> </property> <property> <name>hbase.rpc.timeout</name> <value>60000000</value> </property> <property> <name>hbase.client.scanner.timeout.period</name> <value>60000000</value> </property> <property> <name>phoenix.query.timeoutMs</name> <value>60000000</value> </property> 优点: 灵活,并发、超时时间可以按需调整,无需重启Phoenix集群,对线上服务影响小 缺点: 需要单独build索引的资源机器 受制于build索引机器的单机性能,扩展性差。 异步构建-MR MR索引构建适用于超大规模数据的情况。 使用方式: 异步索引构建方案 创建异步索引 CREATE INDEX async_index ON my_schema.my_table (v) ASYNC 提交MR JOB build 索引 hadoop --config /mr-phoenix-conf jar \ /mr-phoenix-conf/ali-phoenix-5.2.4.1-HBase-2.x-client.jar \ org.apache.phoenix.mapreduce.index.IndexTool \ --data-table {DATA_TABLE_XXXX} \ --index-table {INDEX_XXX} \ --output-path hdfs://hbase-cluster/ASYNC_INDEX_TMP ps: ali-phoenix-client获取,先下载重客户端包,解压后,取ali-phoenix-xxxx-HBase-2.x-client.jar 索引Build MR环境准备 自建Hadoop或者购买EMR Hadoop集群 在MR环境中创建mr-phoenix-conf目录 配置云HBASE的zk到hbase-site.xml,并此配置文件添加到mr-phoenix-conf目录 云HBase的zk地址可以从控制台获取。 拷贝以下hadoop配置文件到mr-phoenix-conf目录下,包括: core-site.xml、mapred-site.xml、yarn-site.xml、hdfs-site.xml 修改hdfs-site.xml(mr-phoenix-conf目录下) 增加hbase hdfs访问能力。 云HBase HDFS开端口和NN地址获取,找@云HBase答疑协助。 修改dfs.nameservices 增加hbase-cluster 增加hbase-cluster hdfs相关配置 EMR集群打通HBase集群参考配置 <property> <name>dfs.nameservices</name> <value>emr-cluster,hbase-cluster</value> </property> <property> <name>dfs.client.failover.proxy.provider.hbase-cluster</name> <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value> </property> <property> <name>dfs.ha.automatic-failover.enabled.hbase-cluster</name> <value>true</value> </property> <property> <name>dfs.ha.namenodes.hbase-cluster</name> <value>nn1,nn2</value> </property> <property> <name>dfs.namenode.rpc-address.hbase-cluster.nn1</name> <value>${nn1-host}:8020</value> </property> <property> <name>dfs.namenode.rpc-address.hbase-cluster.nn2</name> <value>${nn2-host}:8020</value> </property> 优点: 可以对任意规模数据进行索引构建, 扩展性强 缺点: 需要准备单独build资源 相对复杂 总结 适用数据量 优点 缺点 轻客户端 0~10GB 简单,无需额外资源 配置参数调整会影响线上服务 重客户端 10GB-512GB 可以灵活配置并发、超时参数,无需重启Phoenix集群 需要单独的构建索引的机器,受制于单机性能 MR异步构建 512GB~TB级 适用于任意规模索引构建,扩展性强 额外MR资源、MR集群配置复杂 一般少量数据直接用轻客户端来做索引构建,对于中小规模的数据推荐用重客户端来构建索引,而大规模数据则推荐用MR进行索引构建。 参考文档: Phoenix 二级索引:http://phoenix.apache.org/secondary_indexing.html AliPhoenix重客户端下载地址:https://hbase-opt.oss-cn-hangzhou.aliyuncs.com/ali-phoenix-5.2.4.1-HBase-2.x-all.tar.gz

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

中原银行 Arthas 实践之路

作者 | 于爽 中原银行系统研发工程师,目前在技术平台室中间件小组从事分布式缓存、消息队列等相关工作。 【Arthas 官方社区正在举行征文活动,参加即有奖品拿哦~点击投稿】 Arthas 是一款 Java应用开源诊断工具,由于其强大的问题排查及诊断能力,自其开源以来广受开发者的关注和使用,多次登顶 GitHub Trending,并得到国内多家技术媒体的推荐分享。 一. 定制化功能改造 Arthas 可以通过简单的命令交互模式,接入运行的 JVM,快速定位和诊断线上程序运行问题。在不重启服务的情况下,实时、动态的修改相关 code,并实时生效。具体工作原理如下: 1. 连接JVM:通过attach机制,通过attach pid连接正在运行的JVM; 2. 查看及修改JVM字节码:通过instrument技术对运行中的JVM附加或修改字节

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

云迁移的优秀实践

云迁移是将数据和应用程序从现场IT基础设施迁移到云平台的过程,仍然是许多企业的首要任务。事实上根据研究和预测,到2020年,全球将有83%的企业工作负载在云平台中,而AWS和Microsoft Azure等公共云平台将继续主导市场。随着移动设备的广泛采用和采用灵活的工作方式,企业越来越多地转向云平台,以寻求更大的IT敏捷性、可扩展性和业务连续性。 基于云计算的IT系统获得的好处是多方面的,但是在将企业IT系统迁移到云平台,同时确保员工、客户和供应链的“一切照旧”的过程中并非没有挑战。采用强有力的策略将使企业能够最好地获得回报,同时使流程尽可能高效和直接。 实施前需要精心策划 随着全球云计算市场的成熟,越来越多的首席信息官提出令人信服的商业案例来采用云计算。企业将其IT系统迁移到云中可能会产生很大的吸引力,但是实际上是不现实的。并非所有内容都可以迁移或应该迁移,并且还需要考虑迁移的顺序以及对业务和员工的影响。考虑企业的独特需求对于制定可获得云计算优势,而又不影响安全性、日常业务活动、现有遗留系统或浪费预算的计划至关重要。 并非所有内容都将迁移到云平台 许多应用程序和服务仍未针对虚拟环境进行优化,更不用说云计算了。无论企业的云计算战略多么雄心勃勃,都可能会留下大量的数据中心资源处理重要的数据和应用程序。支持这些系统可能是一个持续的挑战,尤其是当企业将更多的重要预算和资源放入云中时。 短期战略和长期战略的需要 针对长期、中期和短期目标制定云迁移策略可能会有所帮助。长期计划可能是将80%的应用程序和数据存储移至云中。但是在短期内,企业将需要考虑在进行云迁移时如何保持现有数据、硬件和应用程序的可访问性和安全性。第三方供应商可以在过渡期间帮助维护原有系统和硬件,以减轻干扰并确保业务连续性。 小心处置废旧硬盘和硬件 云迁移将不可避免地涉及某些硬件的淘汰。从安全角度来看,必须确保所有存储数据的安全,以避免使企业面临数据泄露的风险。许多企业低估了与硬盘相关的安全风险,或者错误地认为常规软件管理方法会提供足够的保护。 云迁移并不意味着减少工作量 尽管投资云计算可以将减少现场硬件和IT经理管理应用程序的数量,但这并不意味着减少工作量。采用云计算需要大量监督工作,以确保供应商满足服务水平协议,控制预算并避免云蔓延。这项至关重要的工作需要不同的技能,因此企业将需要考虑提高员工技能,并对员工进行再培训以管理他们不断变化的角色。 内部部署和云计算集成 云计算还带来了更加难以应对的集成挑战。许多IT管理人员发现,他们必须开发出将基于内部部署的硬件系统与云中的硬件系统集成的方法,以确保数据和应用程序可以彼此协同工作。在许多情况下,这涉及确保网络可以处理各种信息源之间的数据传输。但是,使云计算系统和非云系统相互协作可能非常困难,这不仅涉及难以管理的复杂项目,而且由于用于内部部署数据中心设施的资源较少而变得复杂。 提高IT成本效率 随着将更多预算从现场转移到云计算系统和其他IT外包服务上,许多IT管理人员在现场IT基础设施上的支出减少了。随着越来越多的数据中心需要采用云计算属性来跟上广泛的技术战略,企业的财务压力越来越大。寻找提高IT成本效率的方法对于应对内部部署数据中心维护挑战至关重要。 简化持续的IT成本 随着企业将业务迁移到云平台,随着原有的IT日趋老化,专用的硬件维护和操作系统支持策略使组织与OEM扩展维护服务相比,可以降低维护成本,同时还可以提供更灵活的服务计划。在许多情况下,第三方维护提供商可以为IT管理人员提供所需的服务,而成本却几乎降低了一半。最终结果是,IT团队可以腾出资源用于内部数据中心,并为支持仍在现场环境中运行的系统做好准备。尽管硬件维护计划可能无法解决所有问题,但它们提供了一致的财政和运营措施,使IT团队可以在出现问题时更轻松地管理其数据存储问题。

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

Spark最佳实践-项目规范

前言 大数据开发的日常工作中,开发人员经常需要使用 Spark、Flink 等计算引擎作为工具来实现一些 业务逻辑 的计算。 以 Spark 为例,开发人员会使用 SparkSQL、DataFrame、RDD 等不同形式的API来实现业务需求。 通常情况下,简单的需求都可以通过 SparkSQL、DataFrame 很方便的实现,其简洁的API也是其深受数据分析师青睐的原因之一。 但是正是因为 SparkSQL、DataFrame 的高层次封装,在 复杂度较高的计算需求 实现中,可能会出现 实现复杂或者API的功能性无法满足,或者千方百计实现需求之后 性能表现低下,代码段复杂而庞大 的情况。 尽管Spark允许开发人员通过UDF、UDAF等形式提供自定义的函数功能,但是此时很多人会选择使用较为底层的RDD接口进行开发:可控性好、开发与调试方便、性能强劲。 但是使用RDD接口来开发业务需求时,很多小的项目团队并没有一个统一的项目规范,需求开发完全由开发人员个人自己发挥。 各个业务项目的大致流程基本是相同的: 创建SparkSession 用 spark.table or spark.textFile 等API读取数据源 进行RDD的各种 Transformation 和 Action 操作 得到数据结果之后再调用 saveAsTable or saveAsTextFile 写入外部数据源中 虽然看起来流程挺一致的,但是其中仍然存在以下问题: 业务代码混乱 团队成员代码风格不一,有的喜欢一长串一长串的写,有的喜欢将过程封装 即使将过程封装了,但是封装的边界没有明确定义,仍然会有人不小心“越界” 读写数据源的API使用不统一 各个计算引擎对各个数据源都有不同的读写API接口提供使用,一些比较繁杂的API可能会被人“错误”使用 同时也会有人时常忘记对应接口如何使用,反复查阅资料 重复的编码工作 理论上所有业务项目,除了业务逻辑是变化的之外,其余应该都是一个不变的模板 开发人员应该专注于变化的业务逻辑,而不是每次都要分一些精力出来处理其他“边边角角”的事情 没有规范任由团队成员发挥的话,尽管有些成员能写一手漂亮的代码,但是你并不能保证所有人都这么优秀。 时间一久项目中代码的 坏味道 会越来越多,最后混乱的程度可能会超出你的想象。 为了解决以上问题,我们建议:定义一个项目规范,所有业务项目都需要遵守这个规范。 俗话说,有规矩成方圆。 有了项目规范,所有人都遵守这个标准来开发。 有了这个标准,我们就可以在标准化的基础上做很多事情,比如 定义自动化工具来帮助开发人员解放双手。 本文讨论的项目规范可以作为一种参考,以供读者与相关开发人员翻阅。 一、项目规范 和Java项目规范类似,以 模块化项目 的结构来定义项目规范可以为业务项目提供 结构化标准,其可以规整所有 混乱的业务项目结构。 项目结构标准化的重要性: 项目统一管理与生成 方便快速搭框架 所有开发人员遵守相同的编码规范 易于交接与维护 以下模块划分和Java项目类似,略微有些细节差异。 1.1 api模块 业务计算逻辑模块,不应该出现任何 Spark等执行框架的API 以 保持模块独立性与可移植。 理论上该模块可以独立构成一个单机程序执行,这样可以将最重要的业务逻辑根据需要迁移到任意计算引擎中,如 Spark 到 Spark Streaming、Flink 甚至 Hive UDF 等。 对外只提供接口调用,不可直接在外部实例化具体类(工厂模式) 所有service业务逻辑需要有对应的测试用例 事务控制、所有异常捕获和处理 依赖common 1.2 common模块 项目内通用的常量、枚举、POJO实体类、工具函数等,视情况分离,可集成到 context 中 不包含任何业务逻辑 不依赖其他模块 相关工具保持单例 1.3 context模块 Spark或者其他程序 执行入口,负责初始化各种计算引擎的环境变量。 系统 全局配置(conf)与脚本(bin) 集中管理 依赖server、api、common 程序关键点需要打印日志以便后续debug使用 1.4 server模块 整个项目中整合了业务逻辑调用、数据源读写等操作的模块,需求简单的情况下可以直接集成到 context 中。 该模块中根据不同的接口操作类,还划分了 dal、service与manager三个包。 1.4.1 dal包 主要是对数据进行操作,如读写常用的库:Hive、MySQL、HBase;以及读写文件系统:HDFS。 dal中的所有使用都由接口来定义,不同的接口实现使用不同的应用框架API,如Spark、Flink应该为两个独立的dal实现,在后续service使用过程中可以自由切换。 需要遵循以下原则: 所有bean对象,定义在dal 不得在dal写各种业务逻辑、数据清洗逻辑 一张表对应一个dal接口、一个bean,对应多个独立的dao实现 不允许在1个dao中同时操作多个表 包结构如下: basic: basic包下主要放一些基础对象,如BaseDao,所有dao都需要完善 TABLE_NAME bean: 定义数据源表结构,不同的数据源可以定义在不同的包中,如hive、hbase、mysql等 dao: 接口具体实现,用来操作数据表。如:增删改查 1.4.2 service包 和dao对接,一个service对应一个dao,service的使用都由接口来定义。 一个service下有两个实现包: 正常实现包:直接对接dao,简单处理一些判断:如参数不合法校验等。 测试实现包:模拟数据,可以不通过dao获取,从本地文件生成或代码中生成。 不同的计算框架有不同的service实现,如spark、flink等(需要传入其环境变量)。 1.4.3 manager包 调用service包实现数据增删改查 调用api模块进行业务逻辑组合 提供函数接口给context模块调用执行 二、代码框架 基于以上项目模块的划分,我们可以看到,api、common是 每次都会变化的业务逻辑和通用属性的抽取,而 context 是根据业务需要的计算引擎和运行环境设置的 执行入口。 以上三个模块都是 根据业务需求变化比较大的,而server模块则是负责对 其他各个模块的调用与整合,最后通过 manager 提供统一的函数接口给 context 入口调用执行。 所以 server 模块是这个项目规范中可以 自动化 起来的重点目标。 基于这个目标,我们开发了一个 大数据业务开发 基准项目的雏形,开发人员能够做到开箱即用,不必再花太多精力在研究计算引擎与各个数据源的接口和API如何调用,专注于业务逻辑的实现,提升开发效率。 项目地址:https://github.com/chubbyjiang/aisql-bigdata-base 使用介绍 org.aisql.bigdata.base.framework包中提供了几种常见大数据项目需要用到的数据源。 framework 以 模块化项目 的结构提供了 各个数据源基础的Dao、Service接口与默认实现。 配合自动化的代码生成工具,可以一键生成 server 模块的代码文件直接使用。 现在我们来看一下规范+自动化的威力,例如现有 default.t_users 表需要读取。 开发人员仅需要生成代码文件并复制到项目中,写代码如下: val service = new UsersService //读取整个表 val allRdd:RDD[Users] = service.selectAll() //字段筛选 val allRdd:RDD[Users] = service.selectAllWithCols(Seq("name","age")) //条件过滤 val allRdd:RDD[Users] = service.selectAllByWhere("age>10") //读取1000条数据 val demoRdd:RDD[Users] = service.selectDemo() //写入表 service.insertInto(demoRDD) service.createTable(demoRDD) 是不是 so easy? 其实所做的内容也就是在 server 模块中封装了 常用的不同计算引擎对不同数据源的读写操作API,并 自动化了 bean、dal、service 三个部分的代码生成。 使得开发人员可以直接使用 service 提供的数据操作接口 读出数据源 后 调用业务计算逻辑 处理完毕后 写入数据源 中。 通过对项目模块的标准化规范,我们可以以一个 比较统一和简单易懂的开发方式 来进行需求落地。 虽然刚开始使用规范的时候会有人觉得繁琐与不耐烦,如果是手动开发的话谁都会烦,都是一些重复性的苦力活儿,这就是框架规范的缺点:特别繁琐。 但是配套做一些自动化工具来使用的话,相信大部分开发人员都会觉得很酸爽,某种程度上标准化项目就是这么来提升开发效率的。

资源下载

更多资源
Nacos

Nacos

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

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

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

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册