Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【建议】删除构建阶段的develop分支与ostree,减少文件复制和磁盘损耗,提高构建效率 #739

Open
System233 opened this issue Oct 10, 2024 · 5 comments

Comments

@System233
Copy link

System233 commented Oct 10, 2024

理由

  • 根据以下代码,构建过程中,develop分支是构建阶段的临时存储,而转换到binary分支的操作仅仅近乎等效于cp -arf,完全没有必要
    utils::error::Result<void> splitDevelop(QString installFilepath,
    const QDir &developOutput,
    const QDir &binaryOutput,
    const QString &prefix,
    const std::function<void(int)> &handleProgress)
    {
    LINGLONG_TRACE("split layers file");
    const QString src = developOutput.absolutePath();
    const QString dest = binaryOutput.absolutePath();
    // get install file rule
    QStringList installRules;
    // if ${PROJECT_ROOT}/${appid}.install is not exist, copy all files
    if (QFileInfo(installFilepath).exists()) {
    QFile configFile(installFilepath);
    if (!configFile.open(QIODevice::ReadOnly)) {
    return LINGLONG_ERR("open file", configFile);
    }
    installRules.append(QString(configFile.readAll()).split('\n'));
    // remove empty or duplicate lines
    installRules.removeAll("");
    installRules.removeDuplicates();
    } else {
    qDebug() << "generate install list from " << src;
    QDirIterator iter(src,
    QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System,
    QDirIterator::Subdirectories);
    while (iter.hasNext()) {
    iter.next();
    auto filepath = iter.filePath();
    qDebug() << filepath;
    // $PROJECT_ROOT/.../files to /opt/apps/${appid}
    // $PROJECT_ROOT/ to /runtime/
    filepath.replace(0, src.length(), prefix);
    installRules.append(filepath);
    }
    }
    // 复制目录、文件和超链接
    auto copyFile = [&](const int &percentage,
    const QFileInfo &info,
    const QString &dstPath) -> utils::error::Result<void> {
    LINGLONG_TRACE("copy file");
    if (info.isDir()) {
    qDebug() << QString("percentage: %1%").arg(percentage) << "matched dir"
    << info.absoluteFilePath();
    QDir().mkpath(dstPath);
    return LINGLONG_OK;
    }
    if (info.isSymLink()) {
    qDebug() << QString("percentage: %1%").arg(percentage) << "matched symlinks"
    << info.absoluteFilePath();
    std::array<char, PATH_MAX + 1> buf{};
    // qt的readlin无法区分相对链接还是绝对链接,所以用c库的readlink
    auto size = readlink(info.filePath().toStdString().c_str(), buf.data(), PATH_MAX);
    if (size == -1) {
    qCritical() << "readlink failed! " << info.filePath();
    return LINGLONG_ERR("readlink failed!");
    }
    QString linkpath(buf.data());
    qDebug() << "link" << linkpath << "to" << dstPath;
    QFile file(linkpath);
    if (!file.link(dstPath)) {
    return LINGLONG_ERR("link file failed, relative path", file);
    }
    return LINGLONG_OK;
    }
    // 链接也是文件,isFile要放到isSymLink后面
    if (info.isFile()) {
    qDebug() << QString("percentage: %1%").arg(percentage) << "matched file"
    << info.absoluteFilePath();
    QDir().mkpath(info.path().replace(src, dest));
    QFile file(info.absoluteFilePath());
    if (!file.copy(dstPath)) {
    return LINGLONG_ERR("copy file", file);
    }
    return LINGLONG_OK;
    }
    return LINGLONG_ERR(QString("unknown file type %1").arg(info.path()));
    };
    auto ruleIndex = 0;
    for (auto rule : installRules) {
    // 计算进度
    ruleIndex++;
    auto percentage = ruleIndex * 100 / installRules.length(); // NOLINT
    // 统计进度
    if (handleProgress) {
    handleProgress(percentage);
    }
    // 跳过注释
    if (rule.startsWith("#")) {
    continue;
    }
    // 如果不以^符号开头,当作普通路径使用
    if (!rule.startsWith("^")) {
    // replace $prefix with $PROJECT_ROOT/output/$model/files
    rule.replace(0, prefix.length(), src);
    QFileInfo info(rule);
    // 链接指向的文件如果不存在,info.exists会返回false
    // 所以要先判断文件是否是链接
    if (info.isSymLink() || info.exists()) {
    const QString dstPath = info.absoluteFilePath().replace(src, dest);
    auto ret = copyFile(percentage, info, dstPath);
    if (!ret.has_value()) {
    return LINGLONG_ERR(ret);
    }
    } else {
    qWarning() << "missing file" << rule;
    }
    continue;
    }
    // replace $prefix with $PROJECT_ROOT/output/$model/files
    // TODO(wurongjie) 应该只替换一次, 避免路径包含多个prefix
    // 但不能使用 rule.replace(0, prefix.length), 会导致正则匹配错误
    rule.replace(prefix, src);
    // convert prefix in container to real path in host
    QRegularExpression regexp(rule);
    // reverse files in src
    QDirIterator iter(src,
    QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System,
    QDirIterator::Subdirectories);
    while (iter.hasNext()) {
    iter.next();
    if (regexp.match(iter.fileInfo().absoluteFilePath()).hasMatch()) {
    const QString dstPath = iter.fileInfo().absoluteFilePath().replace(src, dest);
    auto ret = copyFile(percentage, iter.fileInfo(), dstPath);
    if (!ret.has_value()) {
    return LINGLONG_ERR(ret);
    }
    }
    }
    }
    for (const auto &dir : { developOutput, binaryOutput }) {
    // save all installed file path to ${appid}.install
    const auto installRulePath = dir.filePath("../" + QFileInfo(installFilepath).fileName());
    QFile configFile(installRulePath);
    if (!configFile.open(QIODevice::WriteOnly)) {
    return LINGLONG_ERR("open file", configFile);
    }
    if (configFile.write(installRules.join('\n').toUtf8()) < 0) {
    return LINGLONG_ERR("write file", configFile);
    }
    configFile.close();
    }
    return LINGLONG_OK;
    }
  • 导出layer过程中,无法指定仅输出develop或binary,且两个文件始终只差一字节,多出来的字节只是developbinary多一个字母,至少可以说明两个layer文件打包的内容完全相同
    image
    • 导出layer的过程只是对着ostree中的副本目录执行mkfs.erofscat追加到layer中,进一步说明可以完全移除ostree
      utils::error::Result<QSharedPointer<LayerFile>>
      LayerPackager::pack(const LayerDir &dir, const QString &layerFilePath) const
      {
      LINGLONG_TRACE("pack layer");
      QFile layer(layerFilePath);
      if (layer.exists()) {
      layer.remove();
      }
      if (!layer.open(QIODevice::WriteOnly | QIODevice::Append)) {
      return LINGLONG_ERR(layer);
      }
      if (layer.write(magicNumber) < 0) {
      return LINGLONG_ERR(layer);
      }
      // generate LayerInfo
      api::types::v1::LayerInfo layerInfo;
      // layer info version not used yet, so give fixed value
      // keep it for later function expansion
      layerInfo.version = "1";
      auto info = dir.info();
      if (!info) {
      return LINGLONG_ERR(info);
      }
      layerInfo.info = nlohmann::json(*info);
      auto data = QByteArray::fromStdString(nlohmann::json(layerInfo).dump());
      QByteArray dataSizeBytes;
      QDataStream dataSizeStream(&dataSizeBytes, QIODevice::WriteOnly);
      dataSizeStream.setVersion(QDataStream::Qt_5_10);
      dataSizeStream.setByteOrder(QDataStream::LittleEndian);
      dataSizeStream << quint32(data.size());
      Q_ASSERT(dataSizeStream.status() == QDataStream::Status::Ok);
      if (layer.write(dataSizeBytes) < 0) {
      return LINGLONG_ERR(layer);
      }
      if (layer.write(data) < 0) {
      return LINGLONG_ERR(layer);
      }
      layer.close();
      // compress data with erofs
      const auto &compressedFilePath = this->workDir.absoluteFilePath("tmp.erofs");
      const auto &ignoreRegex = QString{ "--exclude-regex=minified*" };
      // 使用-b统一指定block size为4096(2^12), 避免不同系统的兼容问题
      // loongarch64默认使用(16384)2^14, 在x86和arm64不受支持, 会导致无法推包
      auto ret = utils::command::Exec(
      "mkfs.erofs",
      { "-zlz4hc,9", "-b4096", compressedFilePath, ignoreRegex, dir.absolutePath() });
      if (!ret) {
      return LINGLONG_ERR(ret);
      }
      ret = utils::command::Exec(
      "sh",
      { "-c", QString("cat %1 >> %2").arg(compressedFilePath, layerFilePath) });
      if (!ret) {
      LINGLONG_ERR(ret);
      }
      auto result = LayerFile::New(layerFilePath);
      Q_ASSERT(result.has_value());
      return result;
      }

建议

  1. 在临时存储中进行原地后处理,或使用硬链接,减少没有必要的文件复制。
  2. 构建阶段移除ostree,不需要提交任何文件,也不要产生~/.cache/linglong-buidler
  3. ll-builder run 直接挂载临时存储目录

改进效果

  1. 构建过程的磁盘空间占用降低66% (binary, develop, ostree
  2. 导出layer过程的磁盘空间降低50% (binary, develop
  3. 开发者可以极为方便地在宿主机中直接编辑、调整容器内文件,无需ll-builder run进入容器,也无需ll-buidler build --exec 进入shell,更无需反复ll-builer build调试构建脚本
  4. 避免构建阶段的 【BUG】ll-cli/ll-builder磁盘空间泄漏 #740
@black-desk
Copy link
Collaborator

  1. 下一个版本发出来的时候开发模块的功能就会写好 develop部分中并不会再包含binary中的所有内容 这个可以等等

  2. 磁盘的问题的话, 导出出来的layer文件和ostree中的文件数据必须得复制,这个省不了,稍微有点复杂 如果你感兴趣的话我可以详细解释。

  3. 好像是可以考虑把 构建/提交/导出 拆成三个阶段,来避免反复向ostree仓库中提交内容来提升速度 这个我们考虑一下。

@System233
Copy link
Author

  1. 磁盘的问题的话, 导出出来的layer文件和ostree中的文件数据必须得复制,这个省不了,稍微有点复杂 如果你感兴趣的话我可以详细解释。

这一个,layer的内容来自ostree中layers文件夹中的developbinary,而ostree的内容又复制自构建目录下的developbinary文件夹,构建目录下的binary又复制自develop
我觉得可以从develop中一步到位导出layer,唯一要改动的只有测试环境的容器需要直接挂载develop文件夹

如果要做文件裁剪等后处理,也可以直接在develop中操作,开发者能够直观地在宿主机中看到构建工具对文件的改动,也更加透明(目前相同文件复制了多份,很难去找它们之间有什么差异,会对应用产生什么影响,文档中也没有说明技术细节)

@black-desk
Copy link
Collaborator

  1. 做完开发模块拆分以后 develop 下面只会有头文件和调试符号之类的东西
  2. ostree 中的东西checkout到layers目录下这个过程是硬链接的 所以就其实并没有想象中那么大开销

@System233
Copy link
Author

System233 commented Oct 10, 2024

我知道ostree下用了硬链接,只是这样仍旧会产生一次复制,且ostree在构建阶段没有给开发带来功能。

目前改进的develop 拆分是根据文件名自动分类吗?比如.h、.a分离到develop 中,构建过程中通过ostree合并目录再构建?但构建环境实际上是一次性的,每次build都会重新跑一遍构建脚本。ll-builder run现在也不能区分develop 和binary运行环境,也缺少二阶段构建功能。

现在也无法控制develop 的拆分过程,如果构建工具直接拆分文件,反而会引起问题,如我这里有个容器内的32位编译器 gcc-i386-linux-gnu

最好是让开发者去控制如何拆分,yaml中设定构建依赖,然后就可以彻底删除develop了

@black-desk
Copy link
Collaborator

我知道ostree下用了硬链接,只是这样仍旧会产生一次复制,且ostree在构建阶段没有给开发带来功能。

这个问题我们考虑到了,看一下怎么改 感觉直接去掉并不是很好 因为还有其他的打算

目前改进的develop 拆分是根据文件名自动分类吗?比如.h、.a分离到develop 中,构建过程中通过ostree合并目录再构建?但构建环境实际上是一次性的,每次build都会重新跑一遍构建脚本。ll-builder run现在也不能区分develop 和binary运行环境,也缺少二阶段构建功能。

现在也无法控制develop 的拆分过程,如果构建工具直接拆分文件,反而会引起问题,如我这里有个容器内的32位编译器 gcc-i386-linux-gnu

最好是让开发者去控制如何拆分,yaml中设定构建依赖,然后就可以彻底删除develop了

会让开发者可以控制怎么拆的 但是会有个默认规则。

还在写

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants