开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

爱奇艺智能内容中台|无人值守的应用与实践

发表于 2021-11-26

本文为爱奇艺智能内容中台系列稿件的第一弹,接下来我们还会陆续为大家带来爱奇艺在智能内容生产、运营上的一系列探索,敬请期待。

无人值守系统是爱奇艺内容中台的一个重要智能化组件。

首先,对于业务密度大、流程长、依赖服务多的业务系统而言,在实际运行当中,故障的出现是一种常见现象,某种程度上是一种常态。所以故障出现后,能及时发现、及时处理是对线上运行业务系统的必然要求。常规的方式是报警+人工介入,这种方式导致了必须有人值守,而人工介入的时效性不好保证,同时人工成本也必然增加。

爱奇艺内容中台同样面临上述问题,尝试通过技术手段解决系统复杂性带来的问题,是本系统的基本思路,也是中台智能化的重要方向。对比传统的监控系统,我们需要更智能化的服务来完成监控的同时,兼顾故障智能化处理,同时又方便其他处理手段介入。无人值守系统是在上述背景下由爱奇艺内容中台研发团队设计研发的智能化开放式故障值守系统。

一、无人值守的目标

无人值守系统的目标是辅助业务系统达到流程自动化、结果可信赖、无需人工值守。

在项目设计之初,我们的初衷是:在爱奇艺中台节目生产过程中不需要人工盯盘来保障节目生产流程正常、上线准时。在出现问题的时候及时发现、自动修复、风险预警、通知人工处理等以保障节目准时上线。

基于系统的目标,需要系统具有如下能力:

二、整体架构介绍

概括起来,基本思路是:首先实现业务系统运行情况的监控功能,将业务运行数据和异常收集到无人值守系统,在此基础上,通过对数据的实时智能分析,实时发现系统运行故障和业务数据异常情况。然后交由故障和业务异常处理模块进行异常的智能处理,从而达到自动化异常和故障处理,最终实现无人值守和系统智能化恢复的目的。

系统运行流程介绍:

  1. 采集生产环节数据到无人值守系统,这里主要通过爱奇艺的中台数据中心采集。业务系统投递数据到中台数据中心,无人值守系统从中台数据中心采集数据。
  2. 由决策引擎对采集的数据进行实时分析,根据SLA、异常、阈值等配置分析出异常数据,形成单独的事件。
  3. 将事件投递到事件处理引擎Beacon。
  4. 事件处理引擎根据不同事件配置的处理流程进行处理。事件的处理流程完全配置化,支持故障修复、报警通知、故障恢复检测、故障统计等。
  5. 训练引擎使用中台数据中心的离线数据训练系统故障分析模型,模型数据再提供给决策引擎用于决策。

三、核心模块介绍

下面逐一介绍系统的核心功能模块:

3.1 运行数据实时采集模块(基于中台数据中心)

通过爱奇艺内容中台团队实现的OLTP基础组件(中台数据中心),实时采集各个功能模块的运行情况和业务数据进度和状态。OLTP基础组件(中台数据中心)给爱奇艺内容中台实时分析处理提供支持,中台运行的各类数据可以很方便的投递到该组件,并且提供了数据的监听和查询功能,能支持TB集海量数据的管理和维护。中台数据中心系统会在后续的系列文章中详细介绍,这里不再展开。

数据采集流程:

下面以专业内容生产过程(PPC)为例:

制作投递:制作服务运行过程中,将制作状态、成片生产状态,投递中台数据中心。

运营流生产投递:运营流生产过程中将视频流生产状态、音频生产状态、字幕生产状态投递中台数据中心。

审核投递:审核系统将审核状态、审核时间等信息投递中台数据中心。

发布投递:发布系统将码流发布状态兜底中台数据中。

中台数据中心获取到数据后,发送数据变更通知到RMQ队列,无人值守系统监听数据变更,将数据拉取到无人值守系统。从而获取节目从制作到最终发布的运行数据,完成数据采集。

3.2 决策引擎

基于采集的数据,实时分析系统和业务运行情况,决策引擎主要提供如下功能:

错误探查:实时探测系统和业务错误,对错误进行统一管理,发送事件。

超时预警:基于业务节点的SLA配置,对超时业务行为进行探测和统一管理,发送事件。

可配置化策略:主要包括业务功能无人值守接入的配置和业务功能的SLA配置、权限配置、统计通知配置等。

决策引擎的运行就是服务边工作边收集进度及系统运行信息,不断check服务进度是否正常、是否有异常状况或者超时状况,旨在能够在出问题时的第一时间发现问题。

按照下图的逻辑继续工作:

服务模块启动后,发现失败或者超时后,通知失败和超时事件给外部,同时,检测系统业务单元的运行进度,判断进度是否正常,不正常的通知进度滞后事件给外部。

上面介绍了逻辑流程,下面介绍服务模块的运行逻辑:

服务模块的主要概念:

数据源:数据源主要是中台数据中心,播控的部分数据从RMQ采集。数据源的数据由业务系统投递到中台数据中心。

Process:完成数据从数据源采集到无人值守系统,不同的业务分别有不同的process,不同的业务会有不同的采集处理方式。

Filter:完成数据的过滤,采集过来的数据有些是系统不需要的,有些字段是不需要关心的,filter负责把无效的数据过滤掉。

Transform:数据的转换,统一转换成决策需要的数据结构。

规则模块:规则的执行,承接transform过来的数据,执行配置好的规则,输出成功、失败、开始三种规则结果。

决策模块:决策模块是做超时判断的,超时判断和普通的成功、失败不同,需要不停比较当前时间和进度来判断任务是否超时。

延迟消息模块:通过延迟消息,定时未来某个时间来校验业务的运行是否超时。

Sink回写:超时、超预期上线时间等结论回写数据源。

服务模块运行按照上面概念的顺序运行,运行中发现的事件投递给外部。下图:

3.3 事件处理引擎Beacon

事件处理引擎Beacon是自研的接收决策引擎的事件并进行流程化处理的模块。对不同的事件,处理方式千差万别,例如,常见的转码异常有50多种,按照不同的处理方式分成好多组,每组有不同的处理流程,通知不同的研发人员,通知模块内容也不一样。有些事件有标准化的治愈方式,而有些没有治愈方式,需要通知业务人员。针对这样多样化的需要,无人值守的事件处理设计了专门的事件处理引擎(Beacon)。该引擎支持流程的配置化和定制化,预留了抽象度较高的业务对接接口、处理能力扩展方式和流程配置方式。将支持新业务的代价降到了最低。

常见的故障是通过事件的形式发给Beacon处理引擎。

基本架构:

事件处理引擎主要有以下几个部分:

上下文功能:流程执行过程中,上下文参数的获取和保存。上下文的保存和获取实现在Step基类中。

执行引擎:简单可靠的执行引擎,包含了流程执行、执行延迟、执行日志等基础功能,可以执行索引Step类型对象。

流程配置:JSON格式的流程配置,包括Step,StopStrategy,StepAction,WaitTimeAfter等概念,流程按照StepIndex的顺序执行。

Step:步骤,步骤是组成流程的单元,每一步会执行配置的StepAction和StopStrategy。

StopStrategy:流程终止判断策略,Step类的子类,继承了上下文功能。根据配置的状态,判断事件处理的最终形态,比如故障是否恢复的判断。

StepAction:要执行的动作,Step类的子类,比如发邮件、发消息、调业务接口等都可以包装成StepAction。

WaitTimeAfter:本步骤执行完成后,到执行下一步骤的等待时间。

邮件等通知功能对接:对接企业邮件系统等,这是通知功能的基础组件。

业务功能组件:业务功能组件是组成处理流程的基本组件,是系统处理能力的载体。流程处理引擎要支持新业务问题的处理,需要在业务功能组件池进行扩展,从而扩展无人值守的处理能力。

下面是一个简单的流程配置例子**:**

实际使用过程中,针对特定的事件,配置故障处理Action和治愈判断的StopStrategy,从而实现故障自行恢复和治愈。这里故障实际是事件的一种,最后系统能并根据运行数据分析出故障数量和治愈百分比。

四、机器学习应用:生产时间预估

根据无人值守系统的目标设计,无人值守系统要提供对生产过程的预期管理,以实现业务运行可预期的目的。基于采集到的数据,应用机器学习技术,训练各种预测模型,提供业务运行耗时方面的各种预测能力,改进无人值守体验。

说明:无人值守系统的预测模型只适用于资源稳定,或者资源有保障的任务,不考虑资源变动对运行耗时的影响。

下面针对其中的生产时间预估进行说明:

问题类型分析:无人值守系统可以获取丰富的视频生产历史数据,期望形成特征向量,以历史数据计算训练模型,为编辑预估视频生产完成时间。

特征分析:排除空值数据,筛选有价值特征。

比如:类别特征:

‘businessType’, “channel”, ‘cloudEncode’,’trancodeType’, “priority”, “programType”,

“serviceCode”, “needAIInsertFrame”, “needAudit”,”bitrateCode”, “platform”, “resolution”……

数值特征:’duration’

类别特征取值分布分析

算法选择:

训练数据去除异常值,采用XGBoost回归模型。

XGBoost:实现了GBDT算法并进行了算法和工程上的许多改进,(GBDT(Gradient Boosting Decision Tree)梯度提升决策树)。所以叫X (Extreme) GBoosted。

它的基本思路是将基分类器层层叠加,每一层在训练的时候,对前一层基分类器样本,调整不同的权重。测试时,根据各层分类器的结果的加权得到最终结果。所有弱分类器的结果相加等于预测值,然后下一个弱分类器去拟合误差函数对预测值的残差(这个残差就是预测值与真实值之间的误差)。

五、业务系统反馈

无人值守系统为所有对接的业务系统生成业务系统运行情况日报,并推送给业务系统,其中数据主要包括错误、故障统计数据和明细数据,以及是否满足SLA等信息。业务系统基于无人值守反馈的运行数据进行业务改进和系统优化。基于这种方式推动业务系统持续升级改造。

下面是运行情况日报的内容示例:

六、上线效果

在爱奇艺,无人值守系统已经覆盖了爱奇艺内容中台的重要生产环节,对每天几十万的节目生产提供了可靠性报障,无人值守率达到了99%以上,累积发现问题3000多个,自动化处理2800个以上。极大的节约了人力成本,提供了系统运行稳定性和节目按时上线率。

七、未来方向

未来,希望基于无人值守的采集数据提供更多智能化分析,主动发现业务系统的问题,提前预警,提前解决。

另外,系统目前只是针对点状事件进行值守,未来会以节目为整体进行值守和问题处理,提供由点到线,再到面的全方位值守服务。比如以节目为粒度,提供断点续转,后来居上等更加智能化的能力。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Docker+gitlab+jenkins实现项目自动部署(

发表于 2021-11-26

一、Docker安装(CentOS)

1、准备工作

系统要求

以下为官网原文

To install Docker Engine, you need a maintained version of CentOS 7 or 8. Archived versions aren’t supported or tested.

The centos-extras repository must be enabled. This repository is enabled by default, but if you have disabled it, you need to re-enable it.

The overlay2 storage driver is recommended.

Docker 支持 64 位版本 CentOS 7/8,并且要求内核版本不低于 3.10。 CentOS 7 满足最低内核的要求,但由于内核版本比较低,部分功能(如 overlay2 存储层驱动)无法使用,并且部分功能可能不太稳定。

卸载旧版本

旧版本的Docker被称为Docker或Docker -engine。 如果安装了这些组件,运行以下命令进行卸载:

1
2
3
4
5
6
7
8
9
10
bash复制代码$ sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine

CentOS8额外设置

由于 CentOS8 防火墙使用了 nftables,但 Docker 尚未支持 nftables, 我们可以使用如下设置使用 iptables:

更改 /etc/firewalld/firewalld.conf

1
2
bash复制代码# FirewallBackend=nftables
FirewallBackend=iptables

2、使用仓库方式进行安装

设置仓库

在新主机上首次安装Docker引擎之前,需要设置Docker仓库。安装yum-utils包(用到yum-config-manager工具)并设置仓库,鉴于国内网络问题,强烈建议使用国内源,官方源请在注释中查看。

1
2
3
4
5
6
7
8
9
bash复制代码$ sudo yum install -y yum-utils
$ sudo yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

# 官方源
# $ sudo yum-config-manager \
# --add-repo \
# https://download.docker.com/linux/centos/docker-ce.repo

如果需要测试版本的 Docker 请执行以下命令:

1
bash复制代码$ sudo yum-config-manager --enable docker-ce-test

安装Docker引擎

安装最新版本的Docker Engine和容器

1
bash复制代码$ sudo yum install docker-ce docker-ce-cli containerd.io

3、启动Dokcer

1
2
bash复制代码$ sudo systemctl enable docker
$ sudo systemctl start docker

4、测试Docker安装是否正确

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bash复制代码$ docker run --rm hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:cc15c5b292d8525effc0f89cb299f1804f3a725c8d05e158653a563f15e4f685
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

5、后置配置

镜像加速

使用阿里云镜像加速服务

1
2
3
4
5
6
7
8
bash复制代码sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://xxxxxx.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

registry-mirrors参数即镜像加速地址,开通服务获取即可。

添加内核参数

如果在 CentOS 使用 Docker 看到下面的这些警告信息:

1
2
bash复制代码WARNING: bridge-nf-call-iptables is disabled
WARNING: bridge-nf-call-ip6tables is disabled

请添加内核配置参数以启用这些功能。

1
2
3
4
bash复制代码$ sudo tee -a /etc/sysctl.conf <<-EOF
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF

然后重新加载 sysctl.conf 即可

1
bash复制代码$ sudo sysctl -p

二、配置Docker对外开放接口

1、配置对外接口

  • 修改docker配置文件
1
bash复制代码vim /usr/lib/systemd/system/docker.service

找到 ExecStart=/usr/bin/dockerd在后面添加tcp://0.0.0.0:端口号

1
bash复制代码ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:9004 -H fd://

或者直接执行

1
bash复制代码sed -i 's#/usr/bin/dockerd#& -H tcp://0.0.0.0:9004#' /usr/lib/systemd/system/docker.service
  • 重启docker
1
bash复制代码 systemctl restart docker

2、开放端口宿主机访问

  • centos开放端口宿主机访问
1
2
bash复制代码firewall-cmd --add-port=9004/tcp --permanent
firewall-cmd --reload
  • 查询端口是否开启
1
bash复制代码firewall-cmd --query-port=9004/tcp
  • 除此之外还可以开机自启动,将firewalld起起来即可,并且允许其自启动
1
2
bash复制代码systemctl start firewalld 
systemctl enable firewalld

3、查看docker版本信息

  • 浏览器访问:
1
bash复制代码 http://xx.xx.xx.xx:9004/version

看到json信息则表示配置成功

三、安装配置Gitlab

1、拉取Gitlab镜像

1
bash复制代码docker pull gitlab/gitlab-ce

2、构建启动容器

  • 创建gitlab日志,数据,配置的存放路径,通常会在构建容器的时间,会把配置 (etc) 、 日志 (log) 、数据 (data) 放到容器外面,方便后期数据迁移或者修改配置
1
bash复制代码sudo mkdir -p /docker/gitlab/config   /docker/gitlab/logs   /docker/gitlab/data

启动容器

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码sudo docker run --d \
--hostname gitlab \
--publish 8011:443 \
--publish 8012:8012 \
--publish 8013:22 \
--privileged=true \
--name gitlab \
--restart always \
-v /docker/gitlab/config:/etc/gitlab \
-v /docker/gitlab/logs:/var/log/gitlab \
-v /docker/gitlab/data:/var/opt/gitlab \
-v /docker/gitlab/logs/reconfigure:/var/log/gitlab/reconfigure \
gitlab/gitlab-ce:latest

添加配置

1
bash复制代码vim /docker/gitlab/config/gitlab.rb
  • 添加下面3行
1
2
3
4
5
6
7
8
bash复制代码#配置http协议所使用的访问地址,不加端口号默认为80
external_url 'http://xx.xx.xxx.xxx:8012'
#配置ssh协议所使用的访问地址和端口
gitlab_rails['gitlab_ssh_host'] = 'xx.xx.xxx.xxx'
#此端口是run时22端口映射的8013端口
gitlab_rails['gitlab_shell_ssh_port'] = 8013
#保存推出
:wq
  • 看个人需要,一般而已都会限制内存,也可以在docker启动的时候配置,命令为-m 4G
1
css复制代码docker update --memory 4096m --memory-swap -1 gitlab
  • 重启gitlab
1
复制代码docker restart gitlab
  • 进入gitlab容器
1
bash复制代码docker exec -it gitlab bash
  • 重新载入配置文件,并开启
1
2
bash复制代码gitlab-ctl reconfigure
gitlab-ctl start

3、修改root用户密码

  • 进入gitlab控制台
1
bash复制代码gitlab-rails console -e production
  • 获得用户数据,修改用户密码
1
2
3
4
5
bash复制代码user = User.where(id: 1).first
user.password='2YkDixw6xJiD/68kCsAZBu9W9ZhGdRlT0YykDYiOvOAE=1'
user.password_confirmation='2YkDixw6xJiD/68kCsAZBu9W9ZhGdRlT0YykDYiOvOAE=1'
user.save!
quit
  • 重启gitlab需要等待一段时间才能访问,否则会出现502,如果出现502,还有可能是内存不够的原因,建议查看docker容器的gitlab日志
1
bash复制代码docker restart gitlab

四、安装配置Jenkins

1、准备工作

安装jdk

  • 上传jdk压缩包到自定义目录,这里提供从官网拉下来的jdk1.8的linux版本

链接: pan.baidu.com/s/1DQxkbr4J…

提取码: qxu5

  • 解压jdk压缩包到指定目录
1
bash复制代码tar -zxvf jdk-8u202-linux-x64.tar.gz -C /opt/

安装maven

  • 上传jdk压缩包到自定义目录,这里提供从官网拉下来的maven-3.8.4版本

链接: pan.baidu.com/s/198GSQQZT…

提取码: 87qb

1
bash复制代码 tar -zxvf apache-maven-3.8.4-bin.tar.gz -C /opt/

安装git

这里使用yum安装

1
复制代码yum -y install git

配置环境变量

  • 修改环境变量配置文件
1
bash复制代码vim /etc/profile
  • 在最后一行添加java环境变量和maven环境变量
1
2
3
4
5
6
7
8
bash复制代码#java环境配置
export JAVA_HOME=/opt/jdk1.8.0_202
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=./:JAVA_HOME/lib:$JRE_HOME/lib
#maven环境配置
export M2_HOME=/opt/apache-maven-3.8.4
#path
export PATH=/bin:/usr/bin:/sbin:/usr/sbin:$JAVA_HOME/bin:$M2_HOME/bin:$PATH
  • 立即重载配置
1
bash复制代码source /etc/profile

检查安装是否完成

  • java
1
复制代码java -version
  • maven
1
复制代码mvn -v
  • git
1
css复制代码git --version

2、安装jenkins

拉取镜像

1
bash复制代码docker pull jenkinsci/blueocean

运行jenkins容器

1
css复制代码docker run -d --name docker-jenkins -p 8008:8080 -p 50000:50000 jenkinsci/blueocean

访问jenkins主页

浏览器打开ip:8008, 正常情况下会出现解锁jenkins页面

微信截图_20211126093234.png

  • 查看jenkins初始密码
1
bash复制代码docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

将打印出的密码粘贴到管理员密码输入框中, 点击继续进入配置页面,这里选择安装推荐的插件,可能会有安装失败的插件,重试即可

20211126093306250.png

  • 创建用户

插件安装完成后,输入信息创建用户, 点击保存并完成即可进入jenkins主页

微信截图_20211126094104.png

  • jenkins主页

微信截图_20211126102520.png

3、配置

下面对 gitlab 以及 jenkins 进行配置

在容器内生成 ssh 公钥

  • 进入容器
1
bash复制代码docker exec -it jenkins bash
  • 生成密钥
1
bash复制代码ssh-keygen -t rsa -C test@test.com

连续回车三下生成密钥

  • 查看密钥
1
bash复制代码ls ~/.ssh

微信截图_20211126103229.png
可从容器中取出密钥文件

1
bash复制代码docker cp jenkins:/var/jenkins_home/.ssh /tmp/

然后ftp下载文件查看密钥文件内容

gitlab配置公钥

gitlab主页右上角找到Preferences菜单打开,找到SSH Keys栏,将从上面获取id_rsa_pub文件中的公钥配置到SSH keys中

20211126111521445.png

jenkins配置

主要配置介绍

20211126112905379.png

  • 配置凭据
  1. 找到Manage Credentials菜单
  2. 点击全局凭据
  3. 点击添加凭据

进行如下配置:

微信截图_20211126134834.png
其中Key为之前在jenkins容器中生成的RSA私钥, id_rsa文件中的内容

  • 系统配置中配置jdk,maven路径

微信截图_20211126133158.png

  • 全局工具配置,配置jdk,maven,git路径和上面的路径要保持一致(先忽略出现的黄字警告,后面会进行解决)

20211126135346009.png

20211126135400245.png

20211126135406453.png

五、项目配置

1、jenkins新建项目

20211126135833961.png

2、进行如下配置

微信图片_20211126140438.png

3、保存项目并构建

查看控制台输出,出现如下图所示输出表示jenkins已经可以从gitlab上拉取代码到服务器了

20211126140553883.png

接下来的一章介绍的是jenkins正式部署项目,并持续集成,敬请期待。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

linux内核内存虚拟地址映射物理地址

发表于 2021-11-26

先说明下什么是虚拟地址。

Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。虚拟地址空间的内部又被分为内核空间和用户空间两部分。不同字长(也就是单个 CPU 指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。

)

其中,所有进程的内核空间,关联的都是相同的物理内存。进程切换到内核态后,才可以访问内核空间内存。我们下面说到的分段只针对用户空间。

有两种方式管理虚拟地址与物理地址之间的关系。

1、段式管理(Segment):由段选择子和段内偏移量找到物理地址。

)

)

用户空间从低地址到高地址分别是五种不同的内存段。

代码段(只读段),包括代码和常量等。

数据段,包括全局变量等。

堆,包括动态分配的内存,从低地址开始向上增长。

文件映射段,包括动态库、共享内存等,从高地址开始向下增长。(本图没有画出)

栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。

分段容易出现碎片,内存交换效率低(不容易换出到磁盘)的问题。为了解决这两个问题,就出现了内存分页。

2、页式管理(Paging):虚拟地址分为两部分,页号和页内偏移。

)

)

MMU 规定了一个内存映射的最小单位,也就是页,通常是 4 KB 大小。为了解决页表项过多的问题,有多级页表和大页两种方式。

并不是给进程的所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存。这叫程序的局部性原理。根据此原理,为了提高访问速度,MMU(Memory Manage Unit)里配有一个硬件:TLB(Translation Lookaside Buffer)。用于缓存进程常用页表。

内存分段和内存分页并不是对立的,他们组合起来使用,通常称为段页式内存管理。

程序所使用的地址,称为逻辑地址;

通过段式内存管理映射的地址,称为虚拟地址(线性地址);

通过页式内存管理将线性地址映射成物理地址。

)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记一次拿到后台权限的过程

发表于 2021-11-26

前言

本文中涉及到的所有操作均得到了客户单位的授权,并且在漏洞修复后得以公开。请勿在未授权状态下进行任何形式的渗透测试!!!!

漫长的探索

经过初步的信息收集后,发现该医院并无官网,只有一个微信公众号提供了预约挂号,缴费等功能,只能把突破点放在这个公众号上了。

下图是微信公众号的一些功能:

image

当点击这些功能并抓包的时候,令我看到奇怪的是所有的请求都指向了a.test.com这个域名,如下图,原谅我的厚码…

image

【一>所有资源获取<一】
1、200份很多已经买不到的绝版电子书
2、30G安全大厂内部的视频资料
3、100份src文档
4、常见安全面试题
5、ctf大赛经典题目解析
6、全套工具包
7、应急响应笔记

test.com这个域名经过查找后发现是一家提供医疗信息系统的本地公司,但不解的是为什么医院的系统会放到供应商公司去呢?他们是如何进行数据同步的呢?带着这些问题我开始了对a.test.com这个域名的测试。

image

看到这个熟悉的页面,确定了此系统大概是由sping boot开发的,经过一系列常规操作后,只发现了一个swagger-ui页面。

image

由于我们的目标是拿权限,所以重点对此页面的接口进行了sql注入和越权等测试,无果,也没有任何的文件上传接口,开始卡到这里了。

回过来想,a.test.com这个域名是test.com的子域名,是否能够通过test.com进行突破呢?

访问test.com,打开的是供应商公司的官网。

image

对test.com的域名做信息收集后发现了几个子域均解析致某云服务器,但是ip不同。

image

首先git.test.com这个域名引起了我的注意,打开后是一个gitlab服务。

image

gitlab历史上是由几个漏洞的:

image

但不幸的是此系统版本较高,漏洞均以修复。

那会不会由弱口令呢?使用几个常用的用户名和top密码进行爆破,无果,我又想办法加到了此公司的一个qq群中,尝试在群文件中获取一些有效信息。

image

果不然,群文件中有一份表格,记录了员工的详细信息

image

image

有了这些信息,我开始使用姓名和工号等组合成gitlab的用户名,使用常用弱口令定向爆破,期望能有一两个结果,但是还是无果,看来此gitlab对密码强度有要求,弱口令是走不通了。

柳岸花明又一村

当使用google hack 语法搜索此gitlab时发现了几处无需认证即可访问的公开仓库。

image

我开始把希望寄托在了这些可公开访问的仓库上,仔细翻看这些仓库,大多数都是一些接口文档,对本次渗透没有啥用。

终于在rabbitmq安装介绍文档中发现了一个oracle数据库的连接用户名和密码:

image

在前面的信息收集过程中,已经发现了x.test.com这个子域名对应的ip地址开放了oracle数据库端口,我迅速连接了此数据库发现用户名密码正确,可以连接。

image

由于此数据库版本较低,并且时sysdba权限,我可以直接以system权限执行命令。

image

然后就是添加用户登录拿下了这台oracle数据库的服务器。

image

并且在这个mysql文件夹中发现了mysql的配置文件。

image

由于test.com这台服务器是开放了mysql数据库的,利用此信息,我又成功登录了mysql数据库。

image

在mysql数据库中成功获取了供应商官网test.com后台的用户名和密码。

image

当我满怀欣喜的去登录时,发现确实可以登录,但登陆后的后台功能都已废弃,只有一个大大的thinkphp错误。

image

怎么办,原想的通过后台getshell的想法也落空了。(也尝试过使用Thinkphp3的漏洞利用,但也全部失败了)

绝处逢生

到这里,我认为只能把希望放在这个mysql数据库上了,由于是windows系统,udf提权大概率成功不了,那就只能尝试写webshell了。写webshell的话需要知道绝对路径,我尝试使用各种办法让test.com报错,看报错信息中有没有包含绝对路径,一系列操作过后无果,只有404页面。

没办法了,只有盲猜一波,我突然想到了mysql数据库表的表名是否就是网站的目录名呢?

image

使用这两个表名构造了以下绝对路径

1
2
3
4
makefile复制代码c:\\hs_web
c:\\hsweb
d:\\hs_web
d:\\hsweb

当尝试到c:\\hs_web时,webshell提示已经写入成功了。

image

使用蚁剑连接成功:

image

由于当前用户权限较小,使用potato成功提权:

image

添加用户成功登录远程桌面:

image

在服务端的nginx配置文件中发现了代理规则,涉及到几十家医院:

image

原来这些医院的微信公众号业务都是先访问test.com这台服务器,然后再由这台服务器上的nginx转到到各个医院的真实服务器上。那这样也太不安全了吧,一旦供应商的这台服务器宕机、他们的业务也得跟着丢。

然后在这台服务器上发现了微信公众号后台源码,丢给同伴审计了一波,发现了后台登录绕过漏洞,可以直接登录后台。

image

然后就是随意改信息啦。

至此本次渗透就结束了,其实拿到医院的真实ip后也可以更深入的进行测试。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Serverless与微服务探索(二)- SpringBoo

发表于 2021-11-26

前言

上次的文章分享后,有粉丝反应内容太理论太抽象,看不到实际的样子。

因此,我这里就写一篇教程,手把手教你如何把一个SpringBoot项目部署到Serverless并测试成功。

下面的链接是我发表到官方的文章,但官方的文章会综合考虑,所以不会有那么细的步骤。本文是最详细的步骤。

mp.weixin.qq.com/s/0rIkGjYiC…

本文章以腾讯云Serverless云函数为例,将分为事件函数和Web函数两种教程。

事件函数就是指函数是由事件触发的。

Web函数就是指函数可以直接发送HTTP请求触发函数。具体区别可以看这里。

两者在Spring项目迁移改造上的区别在于:

  • 事件函数需要增加一个入口类。
  • Web函数需要修改端口为固定的9000。
  • 事件函数需要操作更多的控制台配置。
  • Web函数需要增加一个scf_bootstrap启动文件,和不一样的打包方式。

事件函数

Spring项目准备

事件函数示例代码下载地址:github.com/woodyyan/sc…

示例代码介绍

@SpringBootApplication 类保持原状不变。

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码package com.tencent.scfspringbootjava8;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ScfSpringbootJava8Application {

public static void main(String[] args) {
SpringApplication.run(ScfSpringbootJava8Application.class, args);
}
}

Controller类也会按照原来的写法,保持不变。这里以todo应用为例子。

记住此处的/todos 路径,后面会用到。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
less复制代码package com.tencent.scfspringbootjava8.controller;

import com.tencent.scfspringbootjava8.model.TodoItem;
import com.tencent.scfspringbootjava8.repository.TodoRepository;
import org.springframework.web.bind.annotation.*;

import java.util.Collection;

@RestController
@RequestMapping("/todos")
public class TodoController {
private final TodoRepository todoRepository;

public TodoController() {
todoRepository = new TodoRepository();
}

@GetMapping
public Collection<TodoItem> getAllTodos() {
return todoRepository.getAll();
}

@GetMapping("/{key}")
public TodoItem getByKey(@PathVariable("key") String key) {
return todoRepository.find(key);
}

@PostMapping
public TodoItem create(@RequestBody TodoItem item) {
todoRepository.add(item);
return item;
}

@PutMapping("/{key}")
public TodoItem update(@PathVariable("key") String key, @RequestBody TodoItem item) {
if (item == null || !item.getKey().equals(key)) {
return null;
}

todoRepository.update(key, item);
return item;
}

@DeleteMapping("/{key}")
public void delete(@PathVariable("key") String key) {
todoRepository.remove(key);
}
}

增加一个ScfHandler类,项目结构如下:

截屏2021-11-09 21.31.31.png

Scfhandle类主要用于接收事件触发,并转发消息给Spring application,然后接收到Spring application的返回后把结果返回给调用方。

默认端口号为8080.

其代码内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
ini复制代码package com.tencent.scfspringbootjava8;

import com.alibaba.fastjson.JSONObject;
import com.qcloud.services.scf.runtime.events.APIGatewayProxyRequestEvent;
import com.qcloud.services.scf.runtime.events.APIGatewayProxyResponseEvent;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

public class ScfHandler {
private static volatile boolean cold_launch;

// initialize phase, initialize cold_launch
static {
cold_launch = true;
}

// function entry, use ApiGatewayEvent to get request
// send to localhost:8080/hello as defined in helloSpringBoot.java
public String mainHandler(APIGatewayProxyRequestEvent req) {
System.out.println("start main handler");
if (cold_launch) {
System.out.println("start spring");
ScfSpringbootJava8Application.main(new String[]{""});
System.out.println("stop spring");
cold_launch = false;
}
// 从api geteway event -> spring request -> spring boot port

// System.out.println("request: " + req);
// path to request
String path = req.getPath();
System.out.println("request path: " + path);

String method = req.getHttpMethod();
System.out.println("request method: " + method);

String body = req.getBody();
System.out.println("Body: " + body);

Map<String, String> reqHeaders = req.getHeaders();
// construct request
HttpMethod httpMethod = HttpMethod.resolve(method);
HttpHeaders headers = new HttpHeaders();
headers.setAll(reqHeaders);
RestTemplate client = new RestTemplate();
HttpEntity<String> entity = new HttpEntity<>(body, headers);

String url = "<http://127.0.0.1:8080>" + path;

System.out.println("send request");
ResponseEntity<String> response = client.exchange(url, httpMethod != null ? httpMethod : HttpMethod.GET, entity, String.class);
//等待 spring 业务返回处理结构 -> api geteway response。
APIGatewayProxyResponseEvent resp = new APIGatewayProxyResponseEvent();
resp.setStatusCode(response.getStatusCodeValue());
HttpHeaders responseHeaders = response.getHeaders();
resp.setHeaders(new JSONObject(new HashMap<>(responseHeaders.toSingleValueMap())));
resp.setBody(response.getBody());
System.out.println("response body: " + response.getBody());
return resp.toString();
}
}

Gradle

这里以gradle为例,与传统开发不一样的地方主要在于,build.gradle中需要加入全量打包的plugin,来保证所有用到的依赖都打入jar包中。

  1. 添加id 'com.github.johnrengelman.shadow' version '7.0.0' 这个plugin。
  2. 添加id 'application'
  3. 添加id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  4. 指定mainClass。

build.gradle具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
bash复制代码plugins {
id 'org.springframework.boot' version '2.5.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java-library'
id 'application'
id 'com.github.johnrengelman.shadow' version '7.0.0'
}

group = 'com.tencent'
version = '0.0.2-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
mavenCentral()
}

dependencies {
api 'org.springframework.boot:spring-boot-starter-web'
api group: 'com.tencentcloudapi', name: 'tencentcloud-sdk-java', version: '3.1.356'
api group: 'com.tencentcloudapi', name: 'scf-java-events', version: '0.0.4'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
useJUnitPlatform()
}

application {
// Define the main class for the application.
mainClass = 'com.tencent.scfspringbootjava8.ScfSpringbootJava8Application'
}

Maven

这里以maven为例,与传统开发不一样的点主要在于,pom.xml需要加入maven-shade-plugin ,来保证所有用到的依赖都打入jar包中。同时需要指定mainClass,下面代码中的mainClass需要改为你自己的mainClass路径。

pom.xml具体内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<!-- Build an executable JAR -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>com.mypackage.MyClass</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
</dependencies>
<configuration>
<keepDependenciesWithProvidedScope>true</keepDependenciesWithProvidedScope>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer
implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
<resource>META-INF/spring.factories</resource>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

编译JAR包

下载代码之后,到该项目的根目录,运行编译命令:

  • Gradle项目运行:gradle build
  • Maven项目运行:mvn package

编译完成后就能在当前项目的输出目录找到打包好的jar包。

  • Gradle项目:在build/libs目录下看到打包好的jar包,这里需要选择后缀是-all的JAR包。如下图。
  • Maven项目:在target目录下能看到打包好的jar包,这里需要选择前缀不带orginal-的jar包。

一会部署函数的时候就用这个JAR包。

云函数准备

云函数创建

在函数服务中,点击新建,开始创建函数。

如下图

  1. 选择自定义创建
  2. 选择事件函数
  3. 输入一个函数名称
  4. 运行环境选择Java8
  5. 提交方法选择本地上传zip包
  6. 执行方法指定为包名.类名::入口函数名
1. 比如此处是:`com.tencent.scfspringbootjava8.ScfHandler::mainHandler`
  1. 上传那里选择前面编译好的带-all后缀的jar包。

截屏2021-11-09 21.39.35.png

然后点击完成创建函数。

云函数配置

创建完成之后,选择函数管理-函数配置-编辑。如下图。
Untitled.png

点开编辑之后,在环境配置中:

  1. 把内存修改为1024MB
  2. 把执行超时时间修改为15秒
    截屏2021-11-10 14.25.57.png

触发器配置

在触发管理中,创建触发器。

1235.png

创建触发器时,在下图中:

  1. 触发方式选择API网关触发。
  2. 集成响应勾选。
  3. 然后提交

5325.png

创建完成之后需要修改一些API网关参数。点击API服务名进入修改。

235.png

点击右侧的编辑按钮修改。

23.png

第一个前端配置中,将路径修改为Spring项目中的默认路径。如下图。

55.png

然后点击立即完成。

然后点击发布服务。

截屏2021-11-09 21.54.29.png

发布完成之后回到云函数控制台。

开始测试

此处我们就以Controller里面写的第一个GET方法为例,如下图,我们将获得所有的todo items。

2345.png

在函数管理中,选择函数代码,就可以很方便的进行测试。如下图。

  1. 测试事件选择“API Gateway事件模版”。
  2. 请求方式选择GET
  3. Path填/todos
  4. 最后就可以点击测试按钮。

4443.png

测试结果和日志将直接显示在界面的右下方。如下图。

2222.png

如果想要获取完整的访问URL,可以在触发管理中,找到刚才创建的API网关触发器,下面有可以访问的URL。URL后面有复制按钮。如下图。

111.png


Web函数

Spring项目准备

示例代码介绍

Web函数示例代码下载地址:github.com/woodyyan/sc…

Web函数的项目代码相比事件函数更简单。代码改造成本几乎没有。对原代码的修改只有一个端口号。

Web函数则不需要ScfHandler入口类,项目结构如下:

666.png

因为web函数必须保证项目监听端口为9000,所以需要将Spring监听的端口改为9000。如下图:

111235.png

代码部署包准备

代码包编译方式参考上面的“编译JAR包”。

然后新建一个scf_bootstrap启动文件,文件名字必须是scf_bootstrap,没有后缀名。

  1. 第一行需有 #!/bin/bash。
  2. java启动命令必须是绝对路径,java的绝对路径是:/var/lang/java8/bin/java
  3. 请确保你的 scf_bootstrap 文件具备777或755权限,否则会因为权限不足而无法执行。

因此启动文件内容如下:

1
2
bash复制代码#!/bin/bash
/var/lang/java8/bin/java -Dserver.port=9000 -jar scf-springboot-java8-0.0.2-SNAPSHOT-all.jar

接着,在scf_bootstrap文件所在目录执行下列命令来保证scf_bootstrap文件可执行。

1
bash复制代码chmod 755 scf_bootstrap

然后将scf_bootstrap文件和刚才编译处理的scf-springboot-java8-0.0.2-SNAPSHOT-all.jar文件,一起打包成zip文件。如下图。

打包好的zip文件就是我们的部署包。

截屏2021-11-11 13.38.02.png

云函数创建

在函数服务中,点击新建,开始创建函数。

如下图

  1. 选择自定义创建
  2. 选择Web函数
  3. 输入一个函数名称
  4. 运行环境选择Java8
  5. 提交方法选择本地上传zip包
  6. 上传那里选择前面压缩好的scf_spring_boot.zip包。

截屏2021-11-11 13.40.28.png

然后在下面的高级配置中,写上启动命令,命令中的jar文件应该是你编译出来的jar文件的名字。

因为web函数必须保证项目监听端口为9000,所以命令中要指定一下端口。

更多关于启动命令的写法可以参考启动文件说明。

如下图:

13ewd.png

然后环境配置那里,把内存改为512MB。执行超时时间设置为15秒。

23we.png

其他设置都使用默认的就可以了。然后点击完成。

点击完成之后如果没有反应,是因为要先等待ZIP文件上传,才会开始创建函数。

因为Web函数默认会创建API网关触发器,因此我们不需要单独配置触发器。

开始测试

此处我们就以Controller里面写的第一个GET方法为例,如下图,我们将获得所有的todo items。

awdz.png

在函数控制台的函数代码里面,我们可以直接测试我们的云函数。

依据上面的代码,我们请求方式选择GET,path填写/todos,然后点击测试按钮,然后就可以在右下角看到我们的结果了。

如果想在其他地方测试,可以复制下图中的访问路径进行测试。

aeawe.png

最后

本教程没有涉及镜像函数,因为镜像部署和原来的部署方式没有差异。项目代码也不需要改造。理论上这是最适合微服务项目的方式。

下一篇文章中,我就会详细分析Serverless中下面几个话题了。

  • Serverless中的服务间调用
  • Serverless中的数据库访问
  • Serverless中的服务的注册与发现
  • Serverless中的服务熔断与降级
  • Serverless中的服务拆分

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

“2021ISIG中国产业智能大会低代码峰会”即将开幕,钉钉

发表于 2021-11-26

简介:2021年12月8-9日,“2021ISIG中国产业智能大会” 将在上海举行。阿里巴巴资深技术专家,钉钉宜搭创始人叶周全将作为特邀嘉宾出席大会。

2021年12月8-9日,由中国电子技术标准化研究院、苏州市金融科技协会、中国计算机用户协会政务信息化分会指导,企智未来科技(RPA中国、LowCode低码时代、信创中国)、机械工业出版社华章分社、CSDN、亿欧EqualOcean等单位联合举办的“2021ISIG中国产业智能大会” 将在上海举行。

阿里巴巴资深技术专家,钉钉宜搭创始人叶周全将作为特邀嘉宾出席大会,并在 “2021低代码技术应用与发展峰会”上以**《钉钉宜搭:云钉低代码,加速构建企业“酷”应用》**为主题,分享钉钉宜搭在低代码赛道的思考与沉淀。

**叶周全(花名骁勇),阿里巴巴资深技术专家,钉钉宜搭创始人。**10+年企业数字化转型实践经验,是阿里集团从信息化到数字化的核心推动者。阿里前端技术委员会核心委员、阿里巴巴低代码发起人,目前负责钉钉宜搭低代码平台。

**钉钉宜搭是阿里巴巴自研的云钉低代码平台,深度融合了钉钉和阿里云的能力,成为“云钉一体”战略中重要组成部分。**从今年1月,宜搭首次亮相钉钉6.0发布会以来,宜搭平台上的应用数已突破100万,覆盖新零售、医疗、生产制造、能源、教育、酒店、金融、运输等主流行业。低代码让越来越多的企业和组织找到了高效、低成本的数字化创新路径,也让个体的需求得到了满足,让个人更有获得感。

**应用可视化搭建,定制灵活:**宜搭提供了可视化拖拽操作界面,将繁琐、复杂的网页元素封装为业务组件,将业务规则、权限等封装为规则配置,用户通过拖拽以及配置,无需编写任何代码即可快速完成符合实际业务需求的应用搭建。

**钉原生:**宜搭作为钉钉官方低代码应用搭建平台,和钉钉能力默认打通。宜搭默认使用钉钉企业通讯录,流程审批可基于组织架构,方便、快捷。支持钉钉消息、钉钉待办,确保重要事项消息必达。搭建好的应用可快速接入钉钉工作台,高效实现在线协同、移动办公。

**云原生:**用宜搭搭建应用,用户只需关注业务本身。数据存储、运行环境、服务器、网络安全等,平台为你全部搞定。此外,借助阿里云,宜搭还提供了强大的弹性计算、动态扩容能力,为你的业务高效、稳定的运行保驾护航。

**模板中心:**借助模板中心用户只需要一分钟即可完成应用的创建,并自动适配电脑端和移动端;用户在搭建完应用后,可一键发布至钉钉工作台。目前,模板中心已覆盖了政务、医疗、教育、餐饮、制造、建筑等主流行业,用户还可以在模板的基础之上进行二次编辑开发。

今年10月的云栖大会上,钉钉宜搭发布了3.0版本,带来了在连接器、数据大屏以及安全方面的重磅更新:3大连接能力套件,让业务互联互通;更低门槛的数据BI能力,一键生成酷炫数字大屏;全局水印、独立域名等安全能力,为用户提供国密级专属安全。

**连接的效率和深度,决定了企业的生产力。**宜搭3大连接能力套件:Excel一键升级企业数字化应用,宜搭连接器以及跨组织流程审批和应用分发,实现了应用与应用之间,应用与人之间,企业上下游之间的全链路连接,宜搭搭建的应用在钉钉上天然互联互通。

**业务数据全面打通,一键生成酷炫大屏。**本次宜搭全新发布了20多款常用报表可视化组件,提供10多款开箱即用的可视化炫酷大屏模版,涵盖销售、人事、生产制造等诸多领域,可广泛用于展示汇报、指挥决策,业务分析等场景。支持100万数据量的表与表关联,和一亿数据量的海量数据处理能力。

**全局水印,独立域名,提供全面安全保障。**保护客户的数据安全是第一原则,宜搭平台依托钉钉全面的安全防护策略,获得公安部信息系统三级等级保护认证,数据享受国密级别安全保护。宜搭上线应用全局水印功能,一旦发生信息泄漏可快速追溯来源。统一风控账号体系+独立企业域名,筑起安全高墙,为企业提供专业级安全防护。

“让人人都具备开发应用的能力,让工作中的每件事情都可被数字化。”是宜搭的努力方向。

阿里云智能总裁、达摩院院长张建锋在刚刚落幕的2021年云栖大会上指出:未来面试,会用低代码或将成为必备技能。他认为随着低代码的进步,大型软件可能会更向专业化细分发展,越来越多的应用软件将由企业自己来开发,企业自己开发应用时将由业务部门使用低代码开发,而非企业的IT部门。

可以预见,未来低代码开发能力将成为个人职场竞争力的重要组成部分。就像今天人人都会使用Office办公软件一样,**明天,低代码开发将成为人人必备的技能。**作为职场人,掌握新生产力工具,提升自我价值,才能紧跟社会发展,成为企业数字化转型的核心人才。

为此,钉钉宜搭推出了低代码开发师的官方认证:

钉钉宜搭先后推出了“低代码开发师”的初级和中级认证,为低代码开发者的技术水平认证制定了行业标准。开发者通过考取官方认证,可以成为当下热门的低代码开发师,提升职场竞争力。宜搭希望通过大家共同的努力,培养更多符合市场需求的低代码人才。

未来,宜搭将在云原生和钉原生能力的加持下,持续深耕低代码领域,携手更多的生态伙伴,助力百行千业的组织实现更低成本的数字化转型,人人都是低代码开发者。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot日志收集以及链路追踪

发表于 2021-11-26

gitee链接

Spring Boot版本:2.3.4.RELEASE

目录

  • 最基本的日志功能及自定义日志
  • 日志链路追踪
  • EFK日志收集系统
  • 用Golang手撸一个轻量级日志收集工具

最基本的日志功能及自定义日志

添加logback依赖:

1
2
3
4
5
xml复制代码<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>5.3</version>
</dependency>

在application配置文件中设置日志保存路径:

1
2
3
4
5
6
7
yml复制代码server:
port: 8888

# 日志保存路径
logging:
file:
path: _logs/mylog-${server.port}.logs

关于logback的配置文件

logback-spring.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--解决在项目目录中生成LOG_PATH_IS_UNDEFINED文件-->
<property name="LOG_PATH" value="${LOG_PATH:-${java.io.tmpdir:-/logs}}"/>
<!-- 引入SpringBoot的默认配置文件defaults.xml -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 引入SpringBoot中内置的控制台输出配置文件console-appender.xml -->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!-- 引入自定义的文件输出配置文件logback-spring-file-level.xml -->
<include resource="logback-spring-file-level.xml"/>

<!-- 设置root logger的级别为INFO,并将控制台输出和文件输出中的appender都添加到root logger下 -->
<root level="INFO">
<!--没有这行,控制台将不会有输出,完全由日志进行输出-->
<appender-ref ref="CONSOLE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>

<!-- jmx可以动态管理logback配置-->
<jmxConfigurator/>
</configuration>

logback-spring-file.level.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<included>
<!--INFO Level的日志-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_PATH}.INFOLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大maxFileSizeMB, 保存maxHistory天的历史日志, 所有日志文件最大totalSizeCapMB -->
<!-- 经过试验,maxHistory是指指定天数内,而不是多少天-->
<maxFileSize>50MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
</rollingPolicy>
<!-- 配置日志的级别过滤器,只保留INFO Level的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 格式化输出-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{"yyyy-MM-dd HH:mm:ss.SSS"} %-5level -[%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>

<!--WARN Level的日志-->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_PATH}.WARNLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大maxFileSizeMB, 保存maxHistory天的历史日志, 所有日志文件最大totalSizeCapMB -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--过滤级别-->
<level>WARN</level>
<!--onMatch:符合过滤级别的日志。ACCEPT:立即处理-->
<onMatch>ACCEPT</onMatch>
<!--onMismatch:不符合过滤级别的日志。DENY:立即抛弃-->
<onMismatch>DENY</onMismatch>
</filter>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{"yyyy-MM-dd HH:mm:ss.SSS"} %-5level -[%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>

<!--ERROR Level的日志-->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_PATH}.ERRORLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大maxFileSizeMB, 保存maxHistory天的历史日志, 所有日志文件最大totalSizeCapMB -->
<maxFileSize>50MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>50MB</totalSizeCap>
<!--<cleanHistoryOnStart>true</cleanHistoryOnStart>-->
</rollingPolicy>
<!--对指定级别的日志进行过滤-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--过滤级别-->
<level>ERROR</level>
<!--onMatch:符合过滤级别的日志。ACCEPT:立即处理-->
<onMatch>ACCEPT</onMatch>
<!--onMismatch:不符合过滤级别的日志。DENY:立即抛弃-->
<onMismatch>DENY</onMismatch>
</filter>
<!--日志输出格式-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{"yyyy-MM-dd HH:mm:ss.SSS"} %-5level - [%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>

<!--自定义日志-->
<appender name="CUSTOM_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- %i用来标记分割日志的序号 -->
<fileNamePattern>${LOG_PATH}.MYLOGGERLevel.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大maxFileSizeMB, 保存maxHistory天的历史日志, 所有日志文件最大totalSizeCapMB -->
<!-- 经过试验,maxHistory是指指定天数内,而不是多少天-->
<maxFileSize>300MB</maxFileSize>
<maxHistory>15</maxHistory>
<totalSizeCap>300MB</totalSizeCap>
</rollingPolicy>
<!-- 配置日志的级别过滤器,只保留INFO Level的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 格式化输出-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{"yyyy-MM-dd HH:mm:ss.SSS"}\t%X{traceId}\t%msg%n</pattern>
</encoder>
</appender>
<!--自定义日志日志不用绑定在root下,只记录指定输出-->
<logger name="my_logger" additivity="false">
<appender-ref ref= "CUSTOM_FILE"/>
</logger>
</included>

配置文件里的注释比较详细,可以根据需要自行修改,配置里有一个“traceId”,这不是logback自带的,是我为了实现日志追踪而添加的,后面会说到。

写接口来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码package com.cc.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;
import java.util.logging.Logger;

@RestController
public class TestController {
Logger LOGGER = Logger.getLogger(this.getClass().toString());
Logger MyLogger = Logger.getLogger("my_logger");

@GetMapping("/w")
public String logWarning() {
LOGGER.warning("这是一段 warning 日志:" + UUID.randomUUID().toString().replace("-", ""));
return "输出 warning 日志";
}

@GetMapping("/e")
public String logError() {
LOGGER.severe("这是一段 error 日志:" + UUID.randomUUID().toString().replace("-", ""));
return "输出 error 日志";
}

@GetMapping("/m")
public String logMyLogger() {
MyLogger.info("这是一段 自定义 日志:" + UUID.randomUUID().toString().replace("-", ""));
return "输出 自定义 日志";
}
}

启动项目,分别执行测试接口,然后我们就可以在_logs文件夹内看到4个日志文件,分别是记录启动信息的INFO日志、记录警告的WARN日志、记录错误的ERROR日志以及自定义的MYLOGGER日志。

日志链路追踪

我们给HTTP请求赋予一个traceId,这个traceId将贯穿整个请求,请求过程中所有的日志都会记录traceId,由此达到快速定位问题和过滤无关日志的效果。

为了好看些,我们定义一个常量类:

1
2
3
4
5
6
7
8
9
10
java复制代码package com.cc.config.logback;

/**
* Logback常量定义
* @author cc
* @date 2021-07-12 10:41
*/
public interface LogbackConstant {
String TRACT_ID = "traceId";
}

然后是logback过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码package com.cc.config.logback;

import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;
import java.util.UUID;

/**
* 日志追踪id
* @author cc
* @date 2021-07-12 10:41
*/
@Component
public class LogbackFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
MDC.put(LogbackConstant.TRACT_ID, UUID.randomUUID().toString().replace("-", ""));
chain.doFilter(request, response);
} finally {
MDC.remove(LogbackConstant.TRACT_ID);
}
}

@Override
public void destroy() {}
}

我们用处理后的UUID作为traceId,现在再调用测试接口,可以看到日志中附带的traceId。

EFK日志收集系统

EFK是指Elasticsearch、Filebeat、和Kibana,原本还有个logstash,但是logstash的使用没有filebeat简单,并且它的内容过滤功能并不是刚需,所以就不加上了,但是后面仍然会附带logstash的简要介绍。

先说明一下EKF的工作流程
  1. Spring Boot应用的日志会保存在指定的路径
  2. filbeat会检测到日志文件的变化,并将内容发送到elasticsearch
    1. 如果使用logstash,则会将内容发送到logstash
    2. logstash将内容进行过滤分析以及格式转换等操作,再发送给elasticsearch,这种处理会使日志数据在kibana上显示的更加详细。
  3. 访问kibana可视化界面,在kibana中操作或查看elasticseach的保存的日志数据
环境准备

因为我的EFK环境是搭建在虚拟机的docker上,本机是Windows,所以为了让docker上的filebeat容器能检测到我的日志文件变化,我有两种方案:

  1. 将项目部署成jar包在虚拟机的Linux上运行,并将日志保存路径设置到指定位置
  2. 本机和虚拟机建立共享文件夹

因为VMWare建立共享文件夹十分简单,并且我也能在本地开发环境实时更新代码,所以选择了方案1。

容器创建

这里假设读者对docker有一定的了解,毕竟关于docker的介绍篇幅不小,而且也与主题无关,就不在这里细说了。

1
2
3
4
5
6
7
8
bash复制代码# 创建一个网络,用于容器间的通讯
docker network create mynetwork

docker run --name myes -p 9200:9200 -p 9300:9300 -itd --restart=always -v /etc/localtime:/etc/localtime -v /home/mycontainers/myes/data:/data --net mynetwork -e "discovery.type=single-node" -e "ES_JAVA_OPTS=-Xms256m -Xmx256m" elasticsearch:7.12.0

docker run --name myfilebeat -itd --restart=always -v /etc/localtime:/etc/localtime -v /mnt/hgfs/myshare/_logs:/data --net mynetwork -v /home/filebeat.yml:/usr/share/filebeat/filebeat.yml elastic/filebeat:7.12.0

docker run --name mykibana -p 5601:5601 -itd --restart=always -v /etc/localtime:/etc/localtime --net mynetwork -m 512m --privileged=true kibana:7.12.0

上面filebeat的容器文件映射路径要注意,映射到了我的共享文件夹,所以不一定和大家一样,按需修改即可。

并且为了方便,我们直接映射了一个filebeat.yml配置文件到filebeat容器内,省的后面再进去修改了。

filebeat.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码filebeat.inputs:
- type: log
enabled: true
paths:
- /data/*.log

output.elasticsearch:
hosts: ['myes']
index: "filebeat-%{+yyyy-MM-dd}"
setup.template.name: "filebeat"
setup.template.pattern: "filebeat-*"

processors:
- drop_fields:
fields: ["log","input","host","agent","ecs"]

配置文件说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码filebeat.inputs:	// 输入源
- type: log // 标注这是日志类型
enabled: true // 启用功能
paths: // 路径
- /data/*.log // filebeat容器内的/data文件夹下,所有后缀为.log的文件

output.elasticsearch: // 输出位置:elasticsearch,后面简称es
hosts: ['myes'] // es的链接,因为我们做了网段所以可以通过容器名进行通讯
index: "filebeat-%{+yyyy-MM-dd}" // 自定义es索引
setup.template.name: "filebeat" // 配置了索引,就需要设置这两项
setup.template.pattern: "filebeat-*" // 配置了索引,就需要设置这两项

processors: // 处理器
- drop_fields: // 过滤或者叫移除指定字段,因为进入es的数据默认会带上这些
fields: ["log","input","host","agent","ecs"]
让kibana连接到elasticsearch

进入kibana容器中,修改配置文件并重启:

docker exec -it mykibana bash

cd config/

vi kibana.yml

原内容:

1
2
3
4
yml复制代码server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
monitoring.ui.container.elasticsearch.enabled: true

修改成:

1
2
3
4
yml复制代码server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://myes:9200" ]
monitoring.ui.container.elasticsearch.enabled: true

可以看出,如果es的容器名就是elasticsearch的话,就可以不用改。

测试容器有效性

elasticsearch:访问 http://ip:9200 ,有json内容出现则成功

kibana:访问 http://ip:5601 ,没有报错,出现可视化UI界面则成功,如果失败,基本是连接问题,请确认配置文件内连接elasticsearch的内容是否正确,确认容器间是否在同一个网段可以进行通讯,调试时可以在容器内互相ping进行确认。

filebeat:等会进kibana可视化界面就能知道

测试效果

调用接口 localhost:8888/w,或者是e/m接口,以输出日志内容到指定位置。此时filebeat已经能检测到文件内容变更并推送到elasticsearch

  • 在指定的目录可以看到输出的日志文件,则说明日志文件保存成功。
  • 打开kibana可视化面板:IP:5061,点击左上角的三横线图标,显示菜单,找到Analytics-Discover,第一次进需要创建Index Patterns,因为我们在filebeat.yml中设置的索引是filebeat,所以这里也要用上,填写了filebeat之后可以看到有匹配项,下一步,步骤2选择时间过滤器,然后确定即可。此时已经可以看到logback->filebeat->elasticsearch的日志内容,然后借助kibana面板就能方便的进行数据检索了。

至此,EFK入门级部署完成。

用Golang手撸一个轻量级日志收集工具

EFK使用方便,界面美观,并且还支持分布式,可以说十分好用了,但是因为我的服务器内存没有那么充裕,用EFK的话要消耗接近1G,所以我选择了另一种方案:用Golang写一个服务,结合Linux的grep指令,从日志文件中提取匹配的内容。这种方案好处是用Golang写,内存占用很低,缺点是搜索效率低,但是对于我的小项目来说正合适。

附上Golang的代码,其实原理很简单,就是使用Gin框架启动一个Web服务,然后调用shell脚本提取内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
go复制代码package main

import (
"fmt"
"os/exec"
"github.com/gin-gonic/gin"
)

func main() {
runServer()
}

func runServer() {
r := gin.Default()

r.GET("/log", func(c *gin.Context) {
id := c.Query("id")
result := runScript("./findLog.sh " + id)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(200, result)
})

r.Run(":18085")
}

func runScript(path string) string {
cmd := exec.Command("/bin/bash", "-c", path)

output, err := cmd.Output()
if err != nil {
fmt.Printf("Execute Shell:%s failed with error:%s", path, err.Error())
return err.Error()
}
fmt.Printf("Execute Shell:%s finished with output:\n%s", path, string(output))
return string(output)
}

findLog.sh:

1
2
3
sh复制代码cd /Users/chen/Desktop/mycontainers/mall-business/data/logs
id=$1
grep $id *.log%

将这个Golang应用打包到指定平台运行即可。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SynchronousQueue 源码分析

发表于 2021-11-26
1
2
3
4
5
6
7
8
9
10
java复制代码// CPU的数量
static final int NCPUS = Runtime.getRuntime().availableProcessors();
// 有超时的情况自旋多少次,当CPU数量小于2的时候不自旋
static final int maxTimedSpins = (NCPUS < 2) ? 0 : 32;
// 没有超时的情况自旋多少次
static final int maxUntimedSpins = maxTimedSpins * 16;
// 针对有超时的情况,自旋了多少次后,如果剩余时间大于1000纳秒就使用带时间的LockSupport.parkNanos()这个方法
static final long spinForTimeoutThreshold = 1000L;
// 传输器,即两个线程交换元素使用的东西
private transient volatile Transferer<E> transferer;

以栈方式实现的Transferer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码static final class TransferStack<E> extends Transferer<E> {
// 栈中节点的几种类型:
// 1. 消费者(请求数据的)
static final int REQUEST = 0;
// 2. 生产者(提供数据的)
static final int DATA = 1;
// 3. 二者正在匹配中
static final int FULFILLING = 2;

// 栈中的节点
static final class SNode {
// 下一个节点
volatile SNode next; // next node in stack
// 匹配者
volatile SNode match; // the node matched to this
// 等待着的线程
volatile Thread waiter; // to control park/unpark
// 元素
Object item; // data; or null for REQUESTs
// 模式,也就是节点的类型,是消费者,是生产者,还是正在匹配中
// 其实就是判断 是消费者节点还是生产者节点
int mode;
}
// 栈的头节点
volatile SNode head;
}

以队列方式实现的Transferer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码static final class TransferQueue<E> extends Transferer<E> {
// 队列中的节点
static final class QNode {
// 下一个节点
volatile QNode next; // next node in queue
// 存储的元素
volatile Object item; // CAS'ed to or from null
// 等待着的线程
volatile Thread waiter; // to control park/unpark
// 是否是数据节点
final boolean isData;
}

// 队列的头节点
transient volatile QNode head;
// 队列的尾节点
transient volatile QNode tail;
}

transfer 栈的方式 入栈 参数e 表示放入的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
scss复制代码E transfer(E e, boolean timed, long nanos) {


SNode s = null;
// 判断方法是执行take 还是put
int mode = (e == null) ? REQUEST : DATA;
// 1 入栈 0 出栈

for (;;)
{
SNode h = head;
// 空栈
if (h == null || h.mode == mode) // empty or same-mode
{
if (timed && nanos <= 0) // can't wait
{
if (h != null && h.isCancelled())
casHead(h, h.next); // pop cancelled node
else
return null;
}
// 走到这里表示是空栈 没有设置超时 cas 乐观锁把head 指向栈顶元素
// e 放入的数据 Mode 1 生产 0 消费 h栈顶元素
// 新加入的元素成为了新的栈顶元素
else if (casHead(h, s = snode(s, e, h, mode)))
{
SNode m = awaitFulfill(s, timed, nanos);
if (m == s)
{ // wait was cancelled
clean(s);
return null;
}
if ((h = head) != null && h.next == s)
casHead(h, s.next); // help s's fulfiller
return (E) ((mode == REQUEST) ? m.item : s.item);
}
}
else if (!isFulfilling(h.mode)) { // try to fulfill
if (h.isCancelled()) // already cancelled
casHead(h, h.next); // pop and retry
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
for (;;) { // loop until matched or waiters disappear
SNode m = s.next; // m is s's match
if (m == null) { // all waiters are gone
casHead(s, null); // pop fulfill node
s = null; // use new node next time
break; // restart main loop
}
SNode mn = m.next;
if (m.tryMatch(s)) {
casHead(s, mn); // pop both s and m
return (E) ((mode == REQUEST) ? m.item : s.item);
} else // lost match
s.casNext(m, mn); // help unlink
}
}
} else { // help a fulfiller
SNode m = h.next; // m is h's match
if (m == null) // waiter is gone
casHead(h, null); // pop fulfilling node
else {
SNode mn = m.next;
if (m.tryMatch(h)) // help match
casHead(h, mn); // pop both h and m
else // lost match
h.casNext(m, mn); // help unlink
}
}
}
}

snode 构造元素

1
2
3
4
5
6
7
8
9
10
ini复制代码static SNode snode(SNode s, Object e, SNode next, int mode) 
{
if (s == null) s = new SNode(e);
//添加还是消费
s.mode = mode;
// 新的元素next 指向加入前的栈顶元素
s.next = next;
// 返回新元素
return s;
}

awaitFulfill

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
java复制代码// 三个参数:需要等待的节点,是否需要超时,超时时间
SNode awaitFulfill(SNode s, boolean timed, long nanos) {
// 到期时间
final long deadline = timed ? System.nanoTime() + nanos : 0L;
// 当前线程
Thread w = Thread.currentThread();
// 自旋次数
int spins = (shouldSpin(s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
// 当前线程中断了,尝试清除s
if (w.isInterrupted())
s.tryCancel();

// 检查s是否匹配到了元素m(有可能是其它线程的m匹配到当前线程的s)
SNode m = s.match;
// 如果匹配到了,直接返回m
if (m != null)
return m;

// 如果需要超时
if (timed) {
// 检查超时时间如果小于0了,尝试清除s
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
if (spins > 0)
// 如果还有自旋次数,自旋次数减一,并进入下一次自旋
spins = shouldSpin(s) ? (spins-1) : 0;

// 后面的elseif都是自旋次数没有了
else if (s.waiter == null)
// 如果s的waiter为null,把当前线程注入进去,并进入下一次自旋
s.waiter = w; // establish waiter so can park next iter
else if (!timed)
// 如果不允许超时,直接阻塞,并等待被其它线程唤醒,唤醒后继续自旋并查看是否匹配到了元素
LockSupport.park(this);
else if (nanos > spinForTimeoutThreshold)
// 如果允许超时且还有剩余时间,就阻塞相应时间
LockSupport.parkNanos(this, nanos);
}
}

// SNode里面的方向,调用者m是s的下一个节点
// 这时候m节点的线程应该是阻塞状态的
boolean tryMatch(SNode s) {
// 如果m还没有匹配者,就把s作为它的匹配者
if (match == null &&
UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
Thread w = waiter;
if (w != null) { // waiters need at most one unpark
waiter = null;
// 唤醒m中的线程,两者匹配完毕
LockSupport.unpark(w);
}
// 匹配到了返回true
return true;
}
// 可能其它线程先一步匹配了m,返回其是否是s
return match == s;
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

旋转字符串 旋转字符串

发表于 2021-11-26

「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」

旋转字符串

字符串左旋

实现一个函数,可以左旋字符串中的k个字符。

例如:

ABCD左旋一个字符得到BCDA

ABCD左旋两个字符得到CDAB

题前认知:

一个字符串如果就定死了。eg:char arr[]=“dfdf”什么的那多没意思,一点都没有人机交互的感觉,(虽然现在人机交互适合个体,不适合集群,但也是比死板的定死字符串舒服)

所以字符串得是我们可输入的,才有可玩性,玩的不尽性就循环

暴力移位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
c复制代码#include<stdio.h>
#include<string.h>

char* turn_left(char* arr, int num)
{
int len = strlen(arr);
int i = 0;
//左旋数是多少就进行几次移位
for (i = 0; i < num; i++)
{
//每次移位就是单次左旋
int j = 0;
char tmp = arr[0];//每次都把首元素存起来
for (j = 0; j < len-1; j++)
{
arr[j] = arr[j + 1];
}
arr[len-1] = tmp;//把存起来的首元素放到最后一位
}
return arr;
}

int main()
{
char arr[100] = {0};
int num = 0;
while (1)
{
printf("请输入想要旋转的字符串:>");
gets(arr);
printf("请输入想要左旋数:>");
scanf("%d", &num);
getchar();//这个的作用是清空缓冲区,因为每次scanf会有一个\n,不清空的话
//下一次到了gets,他会认为\n是我们打进去的,所以scanf之后必须清空缓冲区
printf("左旋字符串为:%s\n", turn_left(arr, num));
}
return 0;
}

输出结果

image-20210909232922661

三步翻转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
c复制代码#include<stdio.h>
#include<string.h>
#include<assert.h>

void reverse(char* head, char* tail) //字符串逆序
{
assert(head && tail);//断言阻止空指针
char tmp = 0;
char* ptmp = &tmp;
while (head<tail)
{
tmp = *head;
*head = *tail;
*tail = tmp;
head++;
tail--;
}
return 0;
}
char* turn_left(char* arr, int num)
{
assert(arr);
int len = strlen(arr);
if (len)
{
int n = num % len; //防止num会越界操作,所以必须是num模len才可,还要保证len不为0
reverse(arr, &arr[n - 1]); //逆序左边
reverse(&arr[n], &arr[len - 1]); //逆序右边
reverse(arr, &arr[len - 1]); //逆序整体
return arr;
}
else
{
return arr;
}
}

int main()
{
char arr[100] = {0};
int num = 0;
while (1)
{
printf("请输入想要旋转的字符串:>");
gets(arr);
printf("请输入想要左旋数:>");
scanf("%d", &num);
getchar();//这个的作用是清空缓冲区,因为每次scanf会有一个\n,不清空的话
//下一次到了gets,他会认为\n是我们打进去的,所以scanf之后必须清空缓冲区
printf("左旋字符串为:%s\n", turn_left(arr, num));
}
return 0;
}

image-20210910003317406

image-20210910100918779

还有3种方法但那三种就需要很强的数学功底了,具体的我也看不懂他怎么证明的数学公式,就不看了。哈哈哈

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

字符串替换空格(多换一) 和 一个智力题 字符串替换空格(多

发表于 2021-11-26

「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」

字符串替换空格(多换一)

面试题:

image-20210910180703837

面试题需要考虑规范,算法,代码,可移植性,还有鲁棒性等,考虑了这些之后再下手就会很舒服

首先我看到是%20替换空格,万一面试官说换其他的然后你很快的改了出来这也是一种本事,所以函数是必不可缺的,也最好有人机交互的样子,反正他有没有说写死.

代码

这里时间复杂度也是O(n),鲁棒性也不错,基本是可以拿下offer的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
c复制代码#include<stdio.h>
#include<assert.h>
#include<string.h>
char* replace(char* parr, const char* e1, const char* e2)
{
assert(e1 && e2);
char arr2[100] = { 0 };//创建一个数组用来存改变后的字符串
unsigned int len = strlen(parr);
unsigned int len1 = strlen(e1);
unsigned int len2 = strlen(e2);
int i = 0;//用来跟踪parr
int j = 0;//用来跟踪arr2
int z = 0;//用来跟踪e2
for (i = 0; i < len; i++)
{
if (parr[i] != *e1)
{
arr2[j++] = *(parr+i);
}
else
{
for (z = 0; z < len2; z++)
{
arr2[j++] = *(e2 + z);
}
}
}
return arr2;
}


int main()
{
char arr[100] = { 0 };//给足空间
while (1)
{
printf("请输入原字符串:>");
gets(arr);
printf("改变后的字符串为:>%s\n", replace(arr, " ", "%20"));
}
return 0;
}

结果

image-20210910203357973

称球次数寻球

题目:13 个球一个天平,现知道只有一个和其它的重量不同,问怎样称才能用三次就找到那个球?

题前分析:

人家把题目设计好的一般肯定3次找到是最少的次数,如果还有其他的那就不是智力题了,题目最重要的还是球的个数,球的个数决定几次找到那个不一样的球,所以又是咱们最爱的找规律(只有一个不一样重的)

一个球

image-20210910221920871

两个球

image-20210910222400352

三个球

这就是最少球数,最小实验就可以进行了

第一次

运气好一次就好

第二次

直接出结果

image-20210910224340864

四个球

到了这里实际上多多少少也可以看出来了

image-20210910232732693

五个球

从这里就开始组合球了,运气好,一次成

第一次运气好直接找到

image-20210910234912828

规律,(第一次找错了,浪费了好久,直接看看有没有数学规律吧)

下面都是数学规律来奠基,大家就不要怀疑了,我尝试过自己找,到头来到最后错了,下面的都改了

十二个球

第一次

第一次同 就是小意思直接顺延给你看看

image-20210913191056502

真正漂亮之处是第一次不同之处

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…172173174…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%