2024年7月

前言

维护GitLab的同事离职了

刚好又有新实习生需要申请账号

只能我来出手了

其实之前安装了 GitLab 之后一直还是用得比较粗糙的

属于是勉强能用的水平,有些配置都还没改好

这次把邮件功能、域名、外观啥的配置好了,写篇文章记录一下

目录结构

先来回顾一下 GitLab 的目录结构

我们的 GitLab 是使用 docker 部署的

gitlab
├── config
├── data
├── logs
├── shell
└── docker-compose.yml

具体的目录结构也是看具体的配置

本文就以这个目录结构为例,进行具体的配置

配置邮件功能

修改
config/gitlab.rb
文件

话说 GitLab 居然是 ruby 写的?
(难怪觉得卡卡的)

编辑邮件配置这一块

这里以腾讯企业邮箱为例

gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.exmail.qq.com"
gitlab_rails['smtp_port'] = 465
gitlab_rails['smtp_user_name'] = "gitlab@codelab.com"
gitlab_rails['smtp_password'] = "email-password"
gitlab_rails['smtp_domain'] = "smtp.exmail.qq.com"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = false
gitlab_rails['smtp_tls'] = false
gitlab_rails['smtp_ssl'] = true
gitlab_rails['smtp_pool'] = false
gitlab_rails['gitlab_email_from'] = "gitlab@codelab.com"

保存与加载配置

配置完成保存一下

进入 GitLab 容器

docker compose exec gitlab bash

执行命令

gitlab-ctl reconfigure
gitlab-ctl restart

搞定

额外邮件配置

对了,还有个额外配置可以改一下,我这里就只修改了
gitlab_email_from
其他的有需要的同学可以改一下

### Email Settings

# gitlab_rails['gitlab_email_enabled'] = true

##! If your SMTP server does not like the default 'From: gitlab@gitlab.example.com'
##! can change the 'From' with this setting.
gitlab_rails['gitlab_email_from'] = 'example@example.com'
gitlab_rails['gitlab_email_display_name'] = 'Example'
gitlab_rails['gitlab_email_reply_to'] = 'noreply@example.com'
gitlab_rails['gitlab_email_subject_suffix'] = ''
gitlab_rails['gitlab_email_smime_enabled'] = false
gitlab_rails['gitlab_email_smime_key_file'] = '/etc/gitlab/ssl/gitlab_smime.key'
gitlab_rails['gitlab_email_smime_cert_file'] = '/etc/gitlab/ssl/gitlab_smime.crt'
gitlab_rails['gitlab_email_smime_ca_certs_file'] = '/etc/gitlab/ssl/gitlab_smime_cas.crt'

发送测试邮件

我用的方法是进入 ruby console 手动发送

在 GitLab 实例的前台页面没找到有什么测试发送的地方

首先进入 GitLab 容器的 shell

docker compose exec gitlab bash

之后启动 ruby console

gitlab-rails console

输入命令发邮件

Notify.test_email('your-email@example.com', 'Test Email', 'This is a test email.').deliver_now

这个方法很好,如果有哪里配置错了,也可以从报错信息里直观的看到

几个跟邮件有关的功能

  • 登录到你的 GitLab 实例。
  • 进入
    Admin Area
    (管理员区域)。
  • 导航到
    Settings
    >
    Integrations

这个页面的中文应该是“实例级集成管理”,我这个版本的 GitLab 有俩跟邮件有关的,分别是:

  • 推送时发送电子邮件
  • 流水线状态电子邮件

顾名思义,前面邮箱配置好了就可以启用这俩

之后满足条件就可以在指定邮箱接收到通知邮件了

重新使用 swag 来分配域名+https

在之前那篇GitLab安装的文章里,我是直接把几个端口都映射出来

现在改成swag做反代,需要修改一下几个地方

例如要分配给 GitLab 的域名是
gitlab.dealiaxy.com

首先修改 docker-compose 配置,把容器加入 swag 网络

version: "3"
services:
  gitlab:
    image: gitlab/gitlab-ee:latest
    restart: always
    hostname: gitlab
    container_name: gitlab
    ports:
      - 9443:443
      - 9022:22
    networks:
      - swag
    volumes:
      - /etc/localtime:/etc/localtime
      - ./config:/etc/gitlab
      - ./logs:/var/log/gitlab
      - ./data:/var/opt/gitlab

networks:
  swag:
    name: swag
    external: true

重启容器

在 swag 里做 GitLab 容器的 80 端口反代

然后
config/gitlab.rb
配置里修改一下

external_url "http://gitlab.dealiaxy.com"

这里使用 HTTP 就好,https 在 swag 那边会配置,如果这里写了 https ,那么会要在 GitLab 里提供证书配置,比较麻烦。

然后 ssh host 也得改一下

gitlab_rails['gitlab_ssh_host'] = gitlab.dealiaxy.com

搞定

外观配置

同样在 Admin Area 里,有个“外观”配置

里面可以修改logo、登录页面的图片、说明(而且还支持markdown)啥的

我稍微改了一下,效果是这样

然后还有 New Project Guidelines 和 Profile Image Guidelines

分别是创建项目和用户修改头像时左侧的引导提示

随便写一点或者用大模型生成就完事了

这样配置下来就有模有样

版本升级

使用
docker-compose pull
命令来拉取最新的镜像后

先 down 然后再 up 就可以使更新生效了~

如果只是 restart 就只会重新启动现有的容器

小结

本文主要还是记录了邮件配置

还有一些比较零散的配置

接下来还要继续完善一下 CICD 流程

到时搞定之后再写文章记录

实验步骤

步骤1: 确认硬盘

确认你的硬盘设备名。

[root@servera ~]# lsblk 
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda      8:0    0   20G  0 disk 
├─sda1   8:1    0  300M  0 part /boot
├─sda2   8:2    0    2G  0 part [SWAP]
└─sda3   8:3    0 17.7G  0 part /
sdb      8:16   0   20G  0 disk 
sdc      8:32   0   20G  0 disk 

步骤2: 分区硬盘(可以不做分区)

对每块硬盘进行分区。

parted /dev/sdb mklabel gpt
parted /dev/sdb mkpart primary ext4 0% 100%
parted /dev/sdc mklabel gpt
parted /dev/sdc mkpart primary ext4 0% 100%

步骤3: 格式化分区(可以不做格式化)

格式化分区为 ext4 文件系统。

mkfs.ext4 /dev/sdb1
mkfs.ext4 /dev/sdc1

步骤4: 创建 RAID 0 阵列(如果前面没有格式化的情况下可以直接使用sdb,sdc)

使用
mdadm
创建 RAID 0 阵列。

mdadm --create /dev/md0 --level=0 --raid-devices=2 /dev/sdb1 /dev/sdc1

步骤5: 检查 RAID 状态

检查 RAID 0 阵列的状态。

cat /proc/mdstat

[root@servera ~]# cat /proc/mdstat
Personalities : [raid0] 
md0 : active raid0 sdc[1] sdb[0]
      41908224 blocks super 1.2 512k chunks
      
unused devices: <none>

步骤6: 格式化 RAID 设备

格式化 RAID 0 设备。

mkfs.ext4 /dev/md0

步骤7: 创建挂载点并挂载

创建挂载点并挂载 RAID 设备。

mkdir /mnt/raid0
mount /dev/md0 /mnt/raid0

[root@servera ~]# mount /dev/md0 /mnt/raid0
[root@servera ~]# lsblk 
NAME   MAJ:MIN RM  SIZE RO TYPE  MOUNTPOINT
sda      8:0    0   20G  0 disk  
├─sda1   8:1    0  300M  0 part  /boot
├─sda2   8:2    0    2G  0 part  [SWAP]
└─sda3   8:3    0 17.7G  0 part  /
sdb      8:16   0   20G  0 disk  
└─md0    9:0    0   40G  0 raid0 /mnt/raid0
sdc      8:32   0   20G  0 disk  
└─md0    9:0    0   40G  0 raid0 /mnt/raid0

步骤8: 测试性能

使用工具测试 RAID 0 的读写性能。

dd if=/dev/zero of=/mnt/raid0/testfile bs=1M count=1024

1024+0 records in
1024+0 records out
1073741824 bytes (1.1 GB) copied, 0.717677 s, 1.5 GB/s

步骤9: 配置开机自动挂载

编辑
/etc/fstab
文件,添加自动挂载配置。

vim /etc/fstab

在文件中添加:

UUID=$(blkid /dev/md0 | grep UUID | awk '{print $2}' | tr -d '"') /mnt/raid0 ext4 defaults 0 0

保存并关闭文件。

步骤10: 重启系统

重启系统以确保所有更改生效。

systemctl reboot

更多技术交流、求职机会,欢迎关注
字节跳动
数据平台
微信公众号

回复【1】
进入官方交流群。
随着互联网的不断发展,企业数据的使用场景也发生巨大变化,湖仓一体逐渐成为一种被广泛应用的底层数据架构。
详细来说,湖仓一体架构是一种将数据湖和数据仓库的优势结合起来的新型数据架构。
数据湖通常以低成本存储大量的原始、多样化的数据,包括结构化、半结构化和非结构化数据,具有很高的灵活性和可扩展性,但在数据治理、数据质量和性能方面可能存在一些挑战。数据仓库则经过了精心的设计和优化,用于处理结构化数据,提供高性能的查询和分析,数据质量和一致性有较好的保障,但在处理多样化数据类型和快速扩展方面相对较弱。
湖仓一体则融合数据湖与数据仓库所长,统一存储降低成本,支持多类型数据,灵活满足多样需求,强化数据治理,保障质量与安全,还兼具高性能分析,快速响应复杂查询。
作为新一代企业级数据产品,火山引擎数智平台在旗下产品中对湖仓一体架构进行深入探索和应用,本篇将从数据湖内核、服务设计及应用场景等方面进行详细拆解。

行业发展趋势

先来了解一下数据平台整体行业的发展趋势,大概分为三个阶段。
第一阶段,一般被称为传统数仓,一种从1980年开始的基于传统数据库技术来做的BI分析场景。在这种架构下,通常计算和存储是高度一体的。整体系统能支撑的计算能力,依赖于服务提供商的硬件配置,整体成本高,存在物理上限,扩展起来比较麻烦。
第二阶段,随着技术的演进, 2010年开始出现了以 Hadoop 技术体系为主流的传统数据湖。在以 Hadoop 技术为主的数据平台架构下,通常可以支持服务在普通硬件上面去部署,整体的计算和存储的扩展性都得到了解决。基于开源技术生态,多个大型公司也参与到数据湖技术发展中来,整体生态繁荣度也在逐步提升。
但在这一阶段凸显出了一个问题,随着生态技术的发展,越来越多的开源组件开始累积。对于一个企业来说,为了解决不同领域的问题,需要运维多个开源的组件,来满足不同领域的数据需求,就导致整个企业的技术运维成本逐步提升。
基于这个问题,随着技术的进一步发展,在2020年,湖仓一体的架构开始被提出。
相比起传统数据湖,湖仓一体架构支持原生的ACID 能力,支持像BI分析、报表分析,机器学习和流式分析多种类型的计算范式,以及云上的对象存储和弹性计算能力。以上能力,让湖仓一体架构能够有效地去解决企业的对数据规模,以及对计算能力的弹性伸缩需求。同时,湖仓一体可以在很大程度上规避传统Lambda架构存在的多个计算组件,或者多种架构范式导致的架构负担,让企业能够更专注地去解决他们的业务价值。
火山引擎数智平台旗下湖仓一体产品为LAS。
从上图来看,LAS架构整体上分为三个部分。最上层是开发工具层,开发工具层会通过计算层提供的统一 SQL 访问服务去访问计算层,根据用户的 SQL 类型自动做SQL解析。所有引擎计算能力统一由弹性容器服务来提供,可以支持弹性伸缩,按需使用。
再往下就是湖仓一体的存储层。首先,湖仓一体存储会通过统一的元数据服务,向计算层提供统一的元数据视图,屏蔽底层的具体元数据实现细节,可以使多个引擎无缝对接到统一的元数据服务。
接下来是湖仓存储引擎,它主要提供了事务管理能力,也就是 ACID的能力,以及对数据批流一体的读写能力。
再往下就是 LAS基于火山引擎对象存储服务TOS和CloudFS ,来提供EB级的数据存储能力和数据访问的缓存加速能力。
以上就是 LAS整体的技术架构。

数据湖
内核
剖析

这一版块将向大家呈现火山引擎数智平台旗下LAS数据湖内核的特性及优化。
数据湖
内核
——
ByteLake
,它是什么?
首先,ByteLake是基于开源Apache Hudi进行内部增强的湖仓一体存储引擎,提供湖仓一体的存储能力。
它的第一个主要能力是提供了湖仓统一的元数据服务,完全兼容开源的Hive Metastore,可以无缝对接多种计算引擎。第二个主要能力是可以支持对海量数据的Insert,完全兼容Hive SQL,可以平迁传统数仓场景下的Hive任务。第三,ByteLake支持对大规模历史数据的Update和Delete,以及对新增数据的Upsert和Append能力。最后,ByteLake支持流批一体的读写能力,提供流式读写的 source 和sink,支持近实时分析。
ByteLake
又是怎么做到这些能力的呢?接下来从以下几个特性来展开阐述。
  • 如何实现高效数据更新?
第一个场景是流式写入更新场景。在这种场景下,最明显的特点就是小批量数据频繁写入更新。但主要的问题是如何去定位要写入的记录呢?是做 update 操作还是 insert 操作?
在这样的背景下,ByteLake提供了一种Bucket Index的索引实现方案。
这是基于哈希的一种索引实现方案。它可以快速地去定位一条记录所对应的Fail Group,从而快速定位当前记录是否已经存在,来判断这一条记录是做Update还是做Insert操作,从而可以快速地将这种小规模的数据去添加到Append Log。在读取时,通过Compaction就可以将LogFile和BaseFile里边的数据进行Merge去重,从而达到数据更新的效果。
针对日志数据入湖,通常来说是不需要主键的,这种基于Hash索引的实现方式,是需要有Shuffle操作的。因为在基于Hash的索引实现中,当一批数据过来之后,会根据这一批数据去找分别对应的File Group,再基于File Group 去聚合要更新的这些数据,通过同一个Task,去更新同一个File Group来实现原子写入。
在数据Shuffle的过程,其实对于数据湖日志写入是有额外的开销的,但ByteLake提供了一种Non index的实现方案,去掉了索引的约束,可以减少数据Shuffle的过程,从而达到快速入湖的能力。
  • 存量数据如何高效更新?
存量数据,一大特点就是数据量大,单表的规模可能有几百 TB ,甚至到 PB 的级别。针对于这种大规模的历史数据的更新场景,如何去提升更新性能?其实最主要的就是要如何去降低数据更新的规模。
基于此,ByteLake提出了一种实现方案——Column Family,将单表多列的场景分别存储到不同列簇。不同的文件可以基于Row Number进行聚合,合并后就是一个完整的行。如果要更新历史数据,只需要去找到要更新的那些列对应的Column Family对应的文件,把这些文件做一些局部更新,就可以达到整体更新的效果。从而在很大程度上减少这些非必要数据的扫描,提升存量历史数据更新场景的性能。
  • 如何提升并发性能?
谈到并发,通常会有两部分内容。比如有很多个任务同时去往ByteLake引擎里边写数据,这就意味着有大批量的任务去访问ByteLake的MetaStore Service。在这种场景下,ByteLake MetaStore Service就会成为一个性能瓶颈。
为了突破这个瓶颈,除了无限的堆加资源之外,另一个比较有效的方案就是增加缓存。通过元数据服务端去缓存比较热点的数据,比如Commit Metadata和Table Metadata,来达到服务端的性能提升。
另外一块,是在引擎侧做优化。比如在Flink引擎层面将Timeline的读取优化到 JobManager 端。同一个任务下,只要JobManager去访问 Hive ByteLake MetaStore Service,缓存到JobManager的本地之后,所有的TaskManager只要去访问JobManager本身缓存的 Timeline 信息就可以了。
从单个任务的视角来看,比如多个任务要同时去更新同一张表,这种情况下要保证数据的正确性,同时又能保证并发性能,应该如何来做?ByteLake提供的解决方案——基于乐观锁的一个并发控制。
针对多任务写同一个表的场景,ByteLake可以支持多种并发策略的设置。业务可以根据对数据一致性的要求,以及对数据并发性能的要求,选择灵活的并发策略,来达到它的数据并发写入的性能指标。

火山引擎数智平台旗下LAS的
数据湖
服务化设计

这个版块将向大家呈现ByteLake服务化过程中的一些设计实践。
  • CatalogService
    :统一的
    元数据
    视图
CatalogService主要提供了与HMS的兼容接口,同时为所有的查询引擎提供了统一的元数据视图,解决了异构数据源的元数据管理问题。
CatalogService 整体分三层,第一层是Catalog Federation,提供统一的视图和跨地域的数据访问能力。以及提供了对源数据请求的路由能力,可以根据元数据请求的类型,支持通过Mapping的方式,来路由不同的服务请求对应的底层元数据服务实例。
第二层是CatalogService下层的具体元数据服务的实现,比如Hive MetaStore Service以及ByteLake MetaStore Service等。可能还有不同的元数据服务对接到CatalogService,来统一向上层引擎提供这种元数据服务。
最后一层是MetaStore的存储层,它通过插件式的方式来提供不同的存储引擎,来满足上层不同元数据服务实例的存储要求。

BMS详解:

湖仓
一体
元数据
管理服务

Bytelake MetaStore Service,简称BMS,它是一个湖仓一体的元数据管理服务,整体的架构分为以下几个部分。首先第一个就是Catalog,Catalog是对单表的元数据访问的抽象。主要逻辑是通过MetaStore Client来访问Meta Server,同时它会去缓存单表的Schema信息以及属性等信息。
另外一部分就是Meta Server,也就是BMS里边最核心的部分。它主要是包含两大部分服务层,第一是Bytelake MetaStore元数据服务模型,比如Table Service,Timeline Service,Partition Service和Snapshot Service。存储层提供了MetaStore所有元数据的存储能力。最后一部分就是Eventbus, Eventbus主要目的是为了将元数据的CUD事件发送给监听者,来达到元数据信息的分发和同步。

元数据
写入流程

关于元数据写入流程,简单来讲,当有一个Client去提交了Instant 之后,Bytelake Catalog会去访问Bytelake Meta Store 的接口,会将Instance改成Completed,然后将请求发到Bytelake的MetaStore,之后Bytelake MetaStore Server 会做一个原子提交。
在此之后,Timeline Service会把提交的状态更新到数据库里边。接下来这些分区信息将再被提交给Partition Service,同步到对应的分区存储表里去。最后一步,把这些所有的变更作为一个快照,同步到 Snapshot Service 里,它会把文件层面的变更存储到数据库里,做持久化存储。

元数据
读取流程

对于源数据的读取流程,举个例子,有一个计算引擎它读取了一个SQL,通过 SQL 解析拿到一张表,这张表会通过Bytelake Catalog Service去请求Bytelake MetaStore,最终会路由到Table Service 拿到这些表的信息。
拿到表的信息做SQL Plan优化的时候,会做一些分区的下推或裁剪。这个时候会去请求到Bytelake的Partition Service做过滤,接着会根据分区信息去扫描文件,在此过程中会去请求Timeline Service获取对应的Timeline信息。接下来,基Timeline的信息时间去Snapshot Service拿到对应文件,再通过 SQL 执行器来实现数据文件的读取。

元数据
变更通知

元数据变更通知具体的实现流程主要依托于两个部分。
一是Eventbus,二是listener。所有的元数据请求都会发送到Eventbus,由Eventbus分发事件到所有已经注册的 Listener上面。 listener再根据下游系统的需求,去订阅Eventbus里边的对应事件类型进行响应,从而达到让上下游的组件感知到元数据的变化,实现元数据的同步。

TMS
详解:

统一表管理服务

LAS的另外一个服务——TMS,全称是 Table Management Service。它主要解决的问题是异步任务的托管优化。为什么会做异步任务的托管优化?因为正常来讲,Flinker SQL 任务写ByteLake表的过程,其实就是把批量的数据写入下游表里边去。随着时间的推移,一个是Commit的日志非常多,另外一个是小文件非常多。通常的Flink引擎层面的实现方案,是在数据写了一定的次数后,追加一个Compaction操作,把之前写入的文件做一个压缩。
但针对流式任务去做Compaction,对正常的流式任务稳定性有很大影响,因为压缩本身是一个开销比较大的动作,对流式计算资源的消耗是很难去评估的,会导致整个流式写入任务的波动,从而影响流式写入任务的稳定性。
基于此,LAS提供了一个统一的表管理服务,异步托管这些本身内置到引擎内部的任务,统一由Table Management Service来托管。它整体的架构是一个主从架构,主要包含的组件一个是Event Receiver,用来接收Metastore下发的一个Event。 PlanGenerator就是根据Meta store Server下发的Event信息,来触发Action Plan的生成。
什么是Action Plan?简单讲,就是这一次要做哪些事情,比如你要做一个压缩任务,还是做一次历史文件的清理,还是做一些小文件的合并,都称为Action Plan。Job Scheduler就是去调度需要被执行的Acting Plan。
什么是Job Manager?它主要用于和集群交互,比如Yarn或K8S,管理Action Plan对应的执行任务,做一些任务运维层面的工作。

执行计划生成

就执行计划生成展开来讲,Plan Generator会接收Metastore下发的一些事件,根据用户在表的DDL里的配置策略,来决定是否要生成执行计划。
这个策略通常会有几种,比如,一种基于它 Delta Commit 的数量,连续提交了多次达到了一定的阈值,就会触发一个Action Plan 的生成,来做一次数据的压缩。另外一种,是根据Log File的大小,来判断Compaction操作是否需要执行。PlanGenerator策略会根据当前 Log File的 Meta 信息,来决定是否要触发Action Plan的生成。

执行计划调度管理

执行计划生成结束之后,最后一步就是怎么去调度管理执行计划。执行计划调度的核心流程主要由Job Scheduler来做,Job Scheduler会定时地去轮询已经生成的Action Plan,再分发给Job Manager。Job Manager拿到了Action Plan之后,会到集群上提交一个任务,同时不断去轮询任务的状态,更新任务的状态到数据库,保证Action Plan执行的可靠性和稳定性。通常JobScheduler一般会有先进先出的调度策略,来保证Action Plan达到预期调度效果。

火山引擎数智平台旗下LAS的业务实践️

抖音电商在
湖仓
一体架构下的业务实践

抖音电商的业务场景,主要是营销大促、流量诊断以及物流状态的监控。他们的业务痛点是什么?数据量大,计算逻辑复杂,同质数据源也比较多,宽表的构建成本比较高,包括一些其他的技术问题。还有一个痛点就是计算周期长,增量计算成本比较高。
基于LAS湖仓一体架构下,可以解决哪些问题呢?
首先,通过LAS快数据入湖能力,可以解决多数据源的快速入湖。把外部的业务系统和业务日志,通过LAS这种实时入湖能力快速导入到ODS层。通过离线数仓可以直接引用ODS层的准实时入库数据,来达到离线数仓的日增量数据,同步提升数据的时效性。
其次,实时数仓中DW层的一些明细数据,也可以通过流式入湖的能力,直接导入到ByteLake,达到数据复用的目的。当把这些数据导到了ByteLake之后,针对大宽表场景,就可以基于ByteLake的多流拼接能力,直接在底层的存储引擎层,实现宽表的构建。从而解决在常规场景下,通过Flink SQL做多源或多流join,导致的任务状态比较大,或者任务处理复杂度比较高的这种稳定性问题,从而更好地去保障业务数据的及时性和稳定性。

消费行业传统
数仓
架构升级

消费行业的客户场景,实际就是在零售场景下的财务管理、库存管理相关的一些计算场景。客户的实现方案基于传统的数据库,业务和离线分析的请求都是统一在一个传统数据库上边来做的。
在这种场景下,其实整个RDBMS要同时承接业务处理逻辑和离线ETL分析逻辑。随着业务数据量的增长,很快就会发现传统数据库的计算能力和存储支撑能力达到了上限,导致计算能力不足,扩展性比较差,无法在满足后续的业务数据规模的上量。
LAS针对这种场景的解决方案,是将客户的离线ETL的分析场景,通过实时集成的方式直接导入到LAS里边,通过LAS的弹性计算能力,为用户的ETL分析场景提供有效的算力保障。在满足客户低成本约束的情况下,达到客户预期的计算效果,和对数据产出的及时性的要求。同时会通过云上的ByteHouse服务来解决客户自建的CK的运维成本以及性能调优的问题。优化了原有的基于RDBMS的数据链路,保证业务数据量快速增长的同时,满足它的底层的算力要求。

湖仓
一体架构下的批流融合计算

典型场景就是数据实时入湖,客户的数据源会通过 Flink SQL 持续地去写入到LAS的Bytelake表里。但下游如果是一个离线任务,其实用户没办法很便利地去判断数据写到了哪个位置,或者分区数据现在是不是已经完备的。
如果仅依赖系统时间来实现,比如在上游的这种Flink SQL任务,在写入过程正常时倒没有特别大的问题。但是一旦上游Flink SQL任务出现一些数据积压或者任务异常的场景,下游依赖系统时间去调度,就会存在某些分区会出现数据空洞或数据偏移的问题。例如本来数据应该落在7点的分区,因为上游的这些 SQL 任务的消费延迟,导致7点的数据并没有准时地落下来, 导致下游去消费7点的数据的时候,拿到的是一个不完整的数据,导致出现数据空洞或数据偏移的问题。
针对这种场景,LAS提供了一种叫归档的能力,也就是在Flink SQL写入的过程中,会基于业务事件时间实时写入对应的数据分区。通过ByteLake提供归档能力,分区数据就绪后,可自动生成一个归档标签。下游的spark SQL 任务可以根据分区是否有归档标签,来判断对应分区的数据是否就绪,来决定当前离线任务是不是要调度起来。
这项能力的实现逻辑,其实就是Flink SQL每次去提交一个Commit的时候,会去判断当前提交的业务的事件时间,是否比当前的未提交分区的时间超过了某一个阈值。比如当前分区的时间是7点,Flink SQL在持续提交微批数据的时候,它判断出来当前的最小的业务时间已经到 7 点半了,而业务定义的可容忍的延迟间隔是 15 分钟, ByteLake认为这个数据其实已经写完了,就会把7点的分区数据打上一个归档标签,来标示数据已经完成了。下游就可以去正常地去消费7点的分区数据,从而保证数据的完整性。
在提供了这种归档能力的情况下,LAS的整体计算链路就可以实现批流融合。比如ODS的ByteLake表是一个准实时的表,下层的Spark SQL任务可以直接通过Spark ETL去做处理,产出一个离线表。可能后边还会有一些SQL场景依赖离线表做数据的准实时消费。在这种情况下,Flink SQL会再生成一张ByteLake表,这张表同样可以被下游的Spark SQL的离线任务依赖,从而达到在整个Pipeline里,做到批流计算相互融合的状态。
火山引擎数智平台通过LAS将湖仓一体架构面向市场输出,期待通过海量数据存储、计算和交互分析能力,以及对Spark、Presto、Hudi等开源生态的兼容能力,助力企业构建云原生智能实时湖仓,帮助企业夯实数智化底座。
点击跳转
火山引擎湖仓一体分析服务LAS
了解更多

本文介绍在
tensorflow
库中,用于
动态调整神经网络的学习率
的一种方法——
指数衰减
ExponentialDecay()
策略的参数含义及其具体用法。

在进行神经网络训练时,我们经常需要用到动态变化的学习率,其中
指数衰减
ExponentialDecay()
策略是我们常用的一种策略。在
tensorflow
库中,其完整的用法是
tf.keras.optimizers.schedules.ExponentialDecay()
,其中的具体参数如下所示。

tf.keras.optimizers.schedules.ExponentialDecay(
    initial_learning_rate, decay_steps, decay_rate, staircase=False, name=None
)

首先,我们需要知道,在用了
ExponentialDecay()
策略后,程序将动态调整神经网络训练过程中的学习率,且这一调整是与我们当前训练的
step
有关的。具体关于
step
的解释,大家可以参考文章
神经网络常见参数解释:epoch、batch、batch size、step、iteration
,本文就不再赘述。

如以下代码所示,使用
ExponentialDecay()
策略后,程序将依据如下的规律,基于当前训练的
step
,以及我们自行设定的几个参数,从而计算得到当前的学习率。其中,函数的返回值就是当前的学习率。

def decayed_learning_rate(step):
  return initial_learning_rate * decay_rate ^ (step / decay_steps)

其中,
initial_learning_rate * decay_rate ^ (step / decay_steps)
就是当前学习率的计算公式。这里的
initial_learning_rate

decay_rate
以及
decay_steps
,就是我们前面提到的
ExponentialDecay()
函数的前
3
个参数。其中,
initial_learning_rate
是我们的初始学习率,
decay_rate
是学习率下降的速率,而
decay_steps
则是学习率下降的位置(具体含义我们稍后介绍)。此外,
ExponentialDecay()
策略还有两个参数,
staircase
表示我们在计算
(step / decay_steps)
时,是对结果
向下取整
还是
取小数
,默认为
False
,即
取小数
结果(具体含义我们稍后介绍);最后一个
name
参数,只是对当前这一学习率下降的策略加以命名,一般用不上这个参数,我们就不再介绍了。

由此,我们可以初步知道,
ExponentialDecay()
函数的前
4
个参数都是用来计算当前的学习率的;且结合我们前面的公式
initial_learning_rate * decay_rate ^ (step / decay_steps)
,我们可以知道,随着当前的
step
不断增加,
decay_rate ^ (step / decay_steps)
是降低的。

接下来,我们直接带入具体的数据,来看一下这几个参数的具体作用。

如下图所示,我们这里有一个训练数据集,其中共有
193608
个样本。

image

同时,我设置了神经网络的
batch size

2048
,那么基于前述提及的文章
神经网络常见参数解释:epoch、batch、batch size、step、iteration
,可知在
1

epoch
中,我们对这
193608
个样本加以训练,共需要的
batch
数目为
193608 / 2048
,也就是
94.54
,向上取整为
95
,相当于需要
95

step
。此外,我设置
initial_learning_rate

decay_rate
以及
decay_steps
分别为
0.1

0.95
以及
95
,且设置
staircase

True
。如下图所示。

此时,我们就可以对每一个参数的具体含义与作用加以介绍了。首先,我们开始训练神经网络模型,即
step
开始从
0
逐步增加;但是由于我的
staircase

True
,因此只要指数
(step / decay_steps)
是小于
1
的,那么都视作
0
(因为当前参数设置是对结果
向下取整
);而由于除了
0
以外任何数的
0
次方都是
1
,因此此时的公式
initial_learning_rate * decay_rate ^ (step / decay_steps)
始终等于
initial_learning_rate
,也就是一直保持
0.1
;只有当
step
到达我们设置的
decay_steps
之后,指数
(step / decay_steps)
才可以成为
1
,使得
decay_rate
终于产生了效果。而在这里,由于我故意设置
decay_steps

95
,因此按道理只要经过
1

epoch
之后,学习率就会下降——因为前面我们计算过了,在
1

epoch
中需要
95

step
。那么此时,学习率就变为了
0.1 * 0.95

接下来,我们运行上述代码,训练
6

epoch
,来验证一下学习率的变化是否如同我们的设想。

下图为
TensorBoard
中,学习率随着
epoch
的变化。这里需要注意,我这里截图的时候开了曲线图的平滑选项,因此应该以浅色的线为准。

上面的图因为不太全,所以或许看不出什么;我们直接将学习率变化情况导出,如下图所示。

其中,图中的
step
实际上表示的是
epoch
,大家这里理解即可。可以看到,在
epoch

0
时(也就是进行第一个
epoch
时),学习率一直为
0.1
;而进行到第二个
epoch
时——此时我们训练过程的
step
就应该是从
95
开始,但还不到
190
,因此
(step / decay_steps)
始终为
1
,学习率就是
0.1 * 0.95 = 0.095
了(因为数据格式问题,精度稍有差距);随后,进行到第三个
epoch
时——此时我们训练过程的
step
就应该是从
190
开始,但还不到
285
,因此
(step / decay_steps)
始终为
2
,学习率就已经是
0.1 * 0.95 * 0.95 = 0.09025
了。

由此可知,假如我将
decay_steps
扩大
10
倍,使得其为
950
,那么在前
10

epoch
时,学习率都不会发生改变,而从第
11

epoch
开始,学习率才会开始衰减。

这里我的参数
staircase
设置为
True
,因此会出现上述结果;相反的,如果设置为
False
,那么计算
(step / decay_steps)
时,是对结果
取小数
,换句话说只要
step
发生变化,那么当前对应的学习率也会发生变化,只不过变化的幅度会稍小一些。

由此看到,上述学习率的变化,是符合我们的预期的。当然,上图中最后两个
epoch
对应的学习率没有发生变化,这个具体原因我暂时也没搞清楚;不过学习率下降作为一种策略,我们通过上述代码,还是达到了动态调整学习率的需求的。

至此,大功告成。

定义

工厂方法模式是一种创建型设计模式,它定义了一个用于创建对象的接口,但由子类来决定实例化哪一个类。工厂方法使得类的实例化延迟到子类,这样可以让客户端在不需要知道具体类的情况下创建对象。
工厂方法模式通过使用继承和多态性,允许子类来控制对象的创建方式,能够更好地应对对象创建的复杂性和变化性。

为什么使用工厂方法模式?

1. 遵循开闭原则

- 工厂方法模式通过引入新的子类来扩展系统,而不需要修改现有代码,从而符合开闭原则。

2. 更灵活的对象创建

- 工厂方法模式将对象创建延迟到子类,这样可以通过重写工厂方法来定制对象的创建过程。

3. 支持产品族的扩展

- 当系统中有多个产品等级结构时,可以通过工厂方法模式来管理不同等级的产品创建过程。

实现步骤

1. 定义抽象产品类

- 定义所有具体产品类的共同接口,客户端将通过这个接口来使用具体产品。

2. 实现具体产品类

- 实现产品接口的具体产品类,这些类包含了产品的实际业务逻辑。

3. 定义抽象工厂类

- 定义一个抽象工厂类,包含一个用于创建产品对象的抽象方法,子类将实现该方法来创建具体产品对象。

4. 实现具体工厂类

- 继承抽象工厂类并实现其抽象方法,具体工厂类负责创建具体产品对象。

优缺点和适用场景

优点

1. 遵循开闭原则

- 新增产品时无需修改已有系统代码,符合开闭原则。

2. 更灵活的对象创建

- 子类可以通过重写工厂方法来定制对象的创建过程,提供更灵活的对象创建机制。

3. 支持产品族扩展

- 能够很好地应对产品族的扩展和变化。

缺点

1. 增加类的数量

- 每新增一种产品类型,都需要增加一个具体工厂类,可能导致系统中类的数量增加。

2. 复杂度增加

- 与简单工厂模式相比,工厂方法模式引入了更多的类和接口,增加了系统的复杂性。

适用场景

1. 系统需要灵活和可扩展的对象创建机制

- 当系统需要灵活地创建对象,并且能够应对产品族的变化时,可以使用工厂方法模式。

2. 遵循开闭原则

- 当系统需要遵循开闭原则,避免修改现有代码来扩展新功能时,适合使用工厂方法模式。

工厂方法模式与简单工厂模式的对比

1. 职责分配

- 简单工厂模式将对象创建集中在一个工厂类中,而工厂方法模式将对象创建延迟到具体子类中,职责更加分散。

2. 开闭原则

- 简单工厂模式在引入新产品时需要修改工厂类,违背了开闭原则;工厂方法模式通过新增具体工厂类来扩展新产品,符合开闭原则。

3. 复杂性

- 简单工厂模式结构较为简单,适用于创建逻辑不复杂的场景;工厂方法模式结构较为复杂,适用于创建逻辑复杂且需要灵活扩展的场景。

咖啡店的例子

我们可以使用工厂方法模式来实现咖啡店不同类型咖啡的创建。
#include <iostream>#include<memory>#include<string>

//抽象产品类:咖啡
classCoffee {public:virtual ~Coffee() {}virtual std::string getDescription() const = 0;virtual double cost() const = 0;
};
//具体产品类:美式咖啡 class Americano : publicCoffee {public:
std::
string getDescription() const override{return "Americano";
}
double cost() const override{return 5.0;
}
};
//具体产品类:拿铁咖啡 class Latte : publicCoffee {public:
std::
string getDescription() const override{return "Latte";
}
double cost() const override{return 6.0;
}
};
//抽象工厂类 classCoffeeFactory {public:virtual ~CoffeeFactory() {}virtual std::shared_ptr<Coffee> createCoffee() const = 0;
};
//具体工厂类:美式咖啡工厂 class AmericanoFactory : publicCoffeeFactory {public:
std::shared_ptr
<Coffee> createCoffee() const override{return std::make_shared<Americano>();
}
};
//具体工厂类:拿铁咖啡工厂 class LatteFactory : publicCoffeeFactory {public:
std::shared_ptr
<Coffee> createCoffee() const override{return std::make_shared<Latte>();
}
};
intmain() {//创建美式咖啡 std::shared_ptr<CoffeeFactory> americanoFactory = std::make_shared<AmericanoFactory>();
std::shared_ptr
<Coffee> americano = americanoFactory->createCoffee();
std::cout
<< "Description:" << americano->getDescription() << ", Cost:" << americano->cost() << "RMB" <<std::endl;//创建拿铁咖啡 std::shared_ptr<CoffeeFactory> latteFactory = std::make_shared<LatteFactory>();
std::shared_ptr
<Coffee> latte = latteFactory->createCoffee();
std::cout
<< "Description:" << latte->getDescription() << ", Cost:" << latte->cost() << "RMB" <<std::endl;return 0;
}