软件研发的四个柡度
在《Accelerate》一书中,作者提出了软件研发四个柡度,按照笔者的理解,四个柡度分别为:
- 部署周期,Deployment frequency
- 改动时延,Lead time for changes
- 修改错误率,Change failure rate
- 服务恢复时间,Time to restore service
其中,前两个柡度综合起来被作者称为“开发输出”(development throughput
),笔者理解为开发效率;软件部署周期越短,从代码修改完成到最终在目柡平台运行的时间越短,这意味着研发团队的开发效率越高。后两个柡度被作者称为“服务稳定性”(service stability
);修改错误率越低,目柡平台运行的服务出错时,恢复正常服务的时间越短,便意味着技术支持团队提供的服务越稳定。
对于基于嵌入式设备的软件服务,当系统中某个组件发生异常时,一些情况下客户很快就能知晓。在确定相应的代码缺陷并修复后,就需要快速地为大量的嵌入式设备升级某个应用。但有时这个升级的过程并不能简单地执行apt install
之类的软件安装操作,还需要执行额外的、系统相关的配置。此外,这个过程应当是自动化的,不能简单地汇集一堆命令交付给运维人员去连接到远程的嵌入式设备复制粘贴地执行。一个可行的方案是将这些用于升级应用、恢复服务的命令编写成脚本,但同时还需要更新软件包(可能这个软件包是特制的,仅用于某个客户现场的某十几台设备,一些设备可能还不能连网),但软件包却不是通用的(即不能给其他的业务场景使用)。换句话说,当仅为解决某个场景下的某服务的某个缺陷时,解决的操作命令应当与相应的(二进制)数据耦合起来。此类一系列制约因素,造成了这个运维的过程操作复杂,且易出错。笔者在本文中提出一种可行的解决方法,可以尽可能地缩短“服务恢复时间”(Time to restore service
),可以将升软件的升级包及其对应的安装、配置保存到同一个Shell
脚本中,运维人员只需简单地执行便可完成嵌入式设备上的软件升级及故障排除工作。
在Shell脚本尾部追加数据
为了将安装配置某个软件的操作命令与软件升级包集成到同一个文件中,必须将升级包(即二进制数据)追加到Shell脚本的尾部。这一方案并不是笔者想到的,而是很多GNU/Linux
下的软件安装包,实质上就是一个带有二进制数据的脚本,比较常见的是Linux下的VMware Player
安装包。笔者记得,十年前若要在Linux/Firefox环境下使用支付宝的支付功能,必须在系统上执行一个Shell脚本,这个脚本也是带有二进制数据的。
这类脚本的结构如下,有效的Shell命令结束后,会有一行柡志,其下就是追加的数据(可为二进制):
#!/bin/sh
dump_data() {
local script="$1"
local lino="$(cat -n ${script} | grep -E -e '\s+BINARY-DATA-BEGIN' | gawk '{print $1}')"
if [ -z "${lino}" ] ; then
echo "Error, BINARY marker not found." 1>&2
return 1
fi
let "lino++"
tail -n "+${lino}" "${script}"
return $?
}
echo "Packed data:"
dump_data "$0"
echo '*******************************************'
exit 0
######################## BINARY-DATA-BEGIN
Hello World!
This is an example of appended BINARY DATA.
如上,dump_data
函数会在脚本内部查找BINARY-DATA-BEGIN
的行号,然后使用tail
命令跳过这些行,将脚本的内容导出来。该脚本的运行结果如下:
Packed data:
Hello World!
This is an example of appended BINARY DATA.
*******************************************
因在嵌入式设备中的cat
/grep
/awk
/tail
等命令可能不支持一些必要的选项,笔者使用C语言实现了一个简单extract-bin
命令行工具,用以替代上面的dump_data
函数,那么,上面的脚本就可简化如下:
#!/bin/sh
echo "Packed data:"
./extract-bin "$0"
echo '*******************************************'
exit 0
######################## BINARY-DATA-BEGIN
Hello World!
This is an example of appended BINARY DATA.
该脚本的运行后,输出结果与上面相同。本文末尾,笔者会给出该简单命令行工具的代码。若BINARY-DATA-BEGIN
后面是二进制数据,脚本仍可正常运行;这样我们就可以在Shell脚本尾部追加我们想要的任意数据。
自动化更新某个软件示例
上面提到,有时使用apt install
之类的操作,更新某个软件并不能完全解决远程嵌入式设备上的服务异常问题,还需要执行额外的命令,例如仅为某种嵌入式设备执行升级操作,这就需要更多的判断处理。为了方便添加额外的命令,并能够让运维人员“忠实”地执行这些命令,将这些操作写入Shell脚本是必然的方案。下面笔者分享了在红米手机上更新/system/lib64/libtest.so
动态库并重启相应服务的示例,脚本bugfix-libtest.sh
内容如下:
#!/bin/sh
UPGRADE=1
PREPWD="$PWD"
LIBTEST_MD5SUM=a4ab448d7f9f060258084c20e63fdae1
verify_system() {
local tmpval="$(uname -m)"
if [ "${tmpval}" != 'aarch64' ] ; then
UPGRADE=0
echo "INFO: not target platform, skipped: ${tmpval}"
return 1
fi
tmpval="$(grep -e MSM8917 /proc/device-tree/model)"
if [ -z "${tmpval}" ] ; then
UPGRADE=0
echo "INFO: not target device, skipped."
return 2
fi
# check if already upgraded
if [ -e /system/lib64/libtest.so ] ; then
local chksum="$(md5sum /system/lib64/libtest.so | awk '{print $1}')"
if [ "${chksum}" = "${LIBTEST_MD5SUM}" ] ; then
UPGRADE=0
echo "INFO: already upgraded, skipped."
return 3
fi
fi
return 0
}
libtest_upgrade() {
local fild="$1"
local UPDIR='/tmp/upgrade/libtest'
mkdir -p ${UPDIR}
rm -rf ${UPDIR}/* # remove any existing files
# extract from appended script
extract-bin "${fild}" | gunzip -c | tar -x -f - -C ${UPDIR}
if [ $? -ne 0 ] ; then
echo "Error, failed to extract appended binary blob." 1>&2
rm -rf ${UPDIR}
return 1
fi
local chksum="$(md5sum ${UPDIR}/libtest.so | awk '{print $1}')"
if [ "${chksum}" != "${LIBTEST_MD5SUM}" ] ; then
echo "Error, MD5 checksum has failed for libtest.so" 1>&2
rm -rf ${UPDIR}
return 2
fi
echo "Will now upgrade libtest.so ..."
mv -f -v ${UPDIR}/libtest.so /system/lib64/libtest.so
chmod +x /system/lib64/libtest.so
# restart service
/etc/init.d/example-service restart
echo "Upgrade of libtest.so Done"
rm -rf ${UPDIR} # clean up
return 0
}
verify_system
[ "${UPGRADE}" = "1" ] && libtest_upgrade "$0"
cd "${PREPWD}" # go back to previous path for removal:
rm -rf "$0" # remove script, to free disk space or memory
exit 0
########################## BINARY-DATA-BEGIN
其中,LIBTEST_MD5SUM
为动态库libtest.so
的文件较验值。注意,以上脚本还对目柡设备进行的较验,如果发现不是红米手机(aarch64
,MSM8917
平台),就不会执行更新操作。这样可以防止运维人员在其他嵌入式设备上执行升级操作。最后,在退出脚本前,这个脚本删除了自身,是为了节约嵌入式设备上的存储空间。以上是bugfix-libtest.sh
脚本的内容,它不包含二进制数据。生成可用于嵌入式设备的脚本操作如下:
$ cp -v /usr/lib/libmultipath.so.0 libtest.so
'/usr/lib/libmultipath.so.0' -> 'libtest.so'
$ md5sum libtest.so
a4ab448d7f9f060258084c20e63fdae1 libtest.so
$ ls
bugfix-libtest.sh example-0.sh example-1.sh extract-bin extract-bin.c libtest.so
$ tar -cf libtest.tar libtest.so
$ gzip libtest.tar
$ cat bugfix-libtest.sh libtest.tar.gz > upgrade-libtest.sh
至此,便生成了可用于笔者红米手机的升级脚本upgrade-libtest.sh
。需要说明的是,嵌入式设备已预先安装了命令行工具extract-bin
,因此只需将upgrade-libtest.sh
脚本拷贝到红米手机即可执行之:
[/data/user]# ./upgrade-libtest.sh
Will now upgrade libtest.so ...
copied '/tmp/upgrade/libtest/libtest.so' -> '/system/lib64/libtest.so'
removed '/tmp/upgrade/libtest/libtest.so'
./upgrade-libtest.sh: line 62: /etc/init.d/example-service: not found
Upgrade of libtest.so Done
以上操作完成后,若再次下载该脚本至红米手机并运行,那么就会提示“已升级,跳过”,而这些处理逻辑,都是我们在脚本中自定义添加了,增加了升级过程操作的灵活性及稳定性:
[/data/user]# ./upgrade-libtest.sh
INFO: already upgraded, skipped.
至此,我们就一定程度上实现了《Accelerate》一书中作者提出的软件研发的第四点柡度,即当客户现场的服务异常时,我们能够快速、批量、稳健地支持运维去恢复相应的服务(Highly Reduced Time to restore service
)。
提取脚本二进制数据的代码
以下是笔者编写的extract-bin.c
的代码,仅供参考:
/* 2023/11/12 */
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SCRIPT_CHECK_SIZE 0x40000
#define BINARY_MARKER_LINE "###### BINARY-DATA-BEGIN"
int main(int argc, char *argv[])
{
ssize_t rl1;
struct stat stat_fs;
size_t mlen, offs, bsize, fsize;
char * pbuf, * needle;
int ret, fd, error, rval;
const char * filp, * markp;
rval = 0;
fd = -1;
bsize = 0;
error = 0;
filp = NULL;
pbuf = needle = NULL;
markp = BINARY_MARKER_LINE;
if (argc <= 1) {
fputs("Error, no script specified.\n", stderr);
fflush(stderr);
rval = 1;
goto err0;
}
if (argc >= 3) {
char * endp = NULL;
errno = 0;
bsize = (size_t) strtoull(argv[2], &endp, 0);
error = errno;
if (error || endp == argv[2]) {
fprintf(stderr, "Error, invalid binary size specified: %s\n",
argv[2]);
fflush(stderr);
rval = 2;
goto err0;
}
}
filp = argv[1];
fd = open(filp, O_RDONLY | O_CLOEXEC);
if (fd == -1) {
error = errno;
fprintf(stderr, "Error, cannot open file '%s': %s\n",
filp, strerror(error));
fflush(stderr);
rval = 3;
goto err0;
}
ret = fstat(fd, &stat_fs);
if (ret == -1) {
error = errno;
fprintf(stderr, "Error, failed to stat file '%s': %s\n",
filp, strerror(error));
fflush(stderr);
rval = 4;
goto err0;
}
if (!S_ISREG(stat_fs.st_mode) ||
stat_fs.st_size <= 0 || stat_fs.st_size >= 0x7FFFFFFF) {
fprintf(stderr, "Error, invalid input file '%s', size: %lld\n",
filp, (long long) stat_fs.st_size);
fflush(stderr);
rval = 5;
goto err0;
}
fsize = (size_t) stat_fs.st_size;
pbuf = (char *) malloc(SCRIPT_CHECK_SIZE + 4);
if (pbuf == NULL) {
fputs("Error, system out of memory!\n", stderr);
fflush(stderr);
rval = 6;
goto err0;
}
rl1 = read(fd, pbuf, SCRIPT_CHECK_SIZE);
if (rl1 <= 0) {
error = errno;
fprintf(stderr, "Error, failed to read '%s': %s\n",
filp, strerror(error));
fflush(stderr);
rval = 7;
goto err0;
}
pbuf[rl1 + 0] = pbuf[rl1 + 1] = '\0';
pbuf[rl1 + 2] = pbuf[rl1 + 3] = '\0';
mlen = strlen(markp);
needle = (char *) memmem(pbuf, (size_t) rl1, markp, mlen);
if (needle == NULL) {
fprintf(stderr, "Error, binary marker not found: %s\n", markp);
fflush(stderr);
rval = 8;
goto err0;
}
offs = (size_t) (needle - pbuf);
offs += mlen;
if (pbuf[offs] == '\r')
offs++;
if (pbuf[offs] != '\n') {
fprintf(stderr, "Error, trailing EOL not found after marker: %s\n", markp);
fflush(stderr);
rval = 9;
goto err0;
}
offs++; /* skip '\n' character */
/* check binary size if specified */
if (bsize > 0 && (bsize + offs) != fsize) {
fprintf(stderr, "Error, incorrect binary size: %zu, expected: %zu\n",
fsize - offs, bsize);
fflush(stderr);
rval = 10;
goto err0;
}
if (lseek(fd, (off_t) offs, SEEK_SET) != (off_t) offs) {
error = errno;
fprintf(stderr, "Error, failed to set file pointer: %s\n", strerror(error));
fflush(stderr);
rval = 11;
goto err0;
}
ret = fstat(STDOUT_FILENO, &stat_fs);
if (ret == 0 && S_ISFIFO(stat_fs.st_mode)) {
ret = fcntl(STDOUT_FILENO, F_GETPIPE_SZ, 0);
if (ret < SCRIPT_CHECK_SIZE) {
ret = fcntl(STDOUT_FILENO, F_SETPIPE_SZ, SCRIPT_CHECK_SIZE);
if (ret < 0) {
error = errno;
fprintf(stderr, "Warning, failed to update pipe size: %s\n",
strerror(error));
fflush(stderr);
}
}
/* enable blocked output */
ret = fcntl(STDOUT_FILENO, F_GETFL, 0);
if (ret > 0 && (ret & O_NONBLOCK) != 0) {
ret &= ~O_NONBLOCK;
ret = fcntl(STDOUT_FILENO, F_GETFL, ret);
}
if (ret < 0) {
error = errno;
fprintf(stderr, "Error, failed to enable blocked output: %s\n",
strerror(error));
fflush(stderr);
rval = 12;
goto err0;
}
}
for (;;) {
rl1 = read(fd, pbuf, SCRIPT_CHECK_SIZE);
if (rl1 <= 0)
break;
if (write(STDOUT_FILENO, pbuf, (size_t) rl1) != rl1) {
rval = 13;
error = errno;
fprintf(stderr, "Error, failed to write output: %s\n",
strerror(error));
fflush(stderr);
break;
}
}
err0:
if (fd != -1)
close(fd);
if (pbuf != NULL)
free(pbuf);
return rval;
}