背景 链接到标题

公司产品最终交付形态是 ISO,在涉及一个产品的多个 OEM 场景时,会选择在标准版本的基础上,删除某些软件包,新增某些软件包的形式来减少构建时间。产品的 BaseOS 是 CentOS,包管理器是 RPM 系,也就需要使用 rpm / yum 等命令来实现。 其中新增某些软件包是使用 yumdownloader 来完成的。在 Yum Repository 中会包含同一软件包的多个版本,预期 yumdownloader 会下载 Yum Repository 中某个软件最新版本的包,比如 yumdownloader zbs-5.1.2* ,则会下载 zbs-5.1.2 大版本的最新 release 版本。

但是最近发现,从某个版本开始 yumdownloader 没有下载最新的软件包,反而停在了一个两个月之前构建的版本,于是开始调查原因。

Yumdownloader 链接到标题

yumdownloader 工具集是由 yum-utils 提供,同时还提供了 repotrackrepoquery, reposync 等有用的工具。yumdownloader 使用方式是 yumdownloader $pkg 即可。在 yum-utils 中会大量引用 yum module,因此需要同时查找两个 Git repo。

yum-utils 代码仓库地址: https://github.com/rpm-software-management/yum-utils/blob/master/yumdownloader.py
yum 代码仓库地址:https://github.com/rpm-software-management/yum

下载逻辑 链接到标题

def main(self):
    # Add command line option specific to yumdownloader
    self.addCmdOptions()
    ...
    # make yumdownloader work as non root user.
    if not self.setCacheDir():
        self.logger.error("Error: Could not make cachedir, exiting")
        sys.exit(50)
    ...
    # Setup yum (Ts, RPM db, Repo & Sack)
    self.doUtilYumSetup()
    # Do the real action
    self.exit_code = self.downloadPackages(opts)
  • 解析命令行参数
  • 检查执行用户权限
  • 配置 Yum Repo 正确性
  • 下载 RPM

其中前几项不是很重要,直接看 self.downloadPackages(opts) 逻辑。主要做了以下几件事情:

  1. 根据 PKG 列表进行 Repo 中的查询,查询出所有的软件包;
  2. 根据查询结果,进行解析匹配,其中精确匹配和模糊匹配会进行后续处理;
  3. 根据 RPM name+arch 作为 key 来将所有的 Pkg 列表转换为 Dict;
  4. 遍历 Dict,从列表中找到 Best Package ,并将其添加到下载列表中;
  5. 根据下载列表信息进行下载。

Github: https://github.com/rpm-software-management/yum-utils/blob/master/yumdownloader.py#L136

def downloadPackages(self,opts):
    
    toDownload = [] # 最终要下载的 RPM

    packages = self.cmds
    for pkg in packages:
        toActOn = []

        if not pkg or pkg[0] != '@':
            pkgnames = [pkg] # 如果 PKG 名称不以 `@` 开头,那么就是单个软件包;如果以 `@` 开头,那么表示是一个 Group;
        else:
            # Group 处理逻辑,忽略
            ...

        pos = self.pkgSack.returnPackages(patterns=pkgnames) # 在 Yum repo 中根据 pkgnames 来获取查询到的包列表
        exactmatch, matched, unmatched = parsePackages(pos, pkgnames) # 根据 pkgnames 名称 从 pos 中解析具体的匹配结果,其中如果精确匹配则添加到 exactmatch,如果模糊匹配则添加到 matched。在本文场景下所有匹配为模糊匹配,所以在 matched 列表中
        installable = (exactmatch + matched) # 最终可安装的是精确匹配和模糊匹配的集合
        if not installable:
            ...

        for newpkg in installable:
            toActOn.extend(_best_convert_pkg2srcpkgs(self, opts, newpkg)) # 根据解析结果,将所有处于 installable 中的包都找到具体来源
        if toActOn:
            pkgGroups = self._groupPackages(toActOn) # 使用 RPM `name` 和 `arch` 针对 toActOn 列表进行初步分组,转换为 dict
            for group in pkgGroups:
                pkgs = pkgGroups[group]
                if opts.source:
                    ...     # 根据 yumdownloader 命令参数进行额外的处理,忽略
                else:
                    toDownload.extend(self.bestPackagesFromList(pkgs)) # 从 pkgs 列表中选择 best Package,然后添加到 toDownload 列表中
                        
    ...
    # set localpaths
    for pkg in toDownload:
        pkg.repo.copy_local = True
        pkg.repo.cache = 0

    # use downloader from YumBase
    exit_code = 0
    probs = self.downloadPkgs(toDownload) # 下载具体 RPM 
    if probs:
        exit_code = 2
        for key in probs:
            for error in probs[key]:
                self.logger.error('%s: %s', key, error)
    return exit_code

在我的场景中,问题出在查找 Best Package 步骤中,相关日志如下,可以观察到在 Yum repo 中查询到了 zbs-5.1.2 所有的 release RPM,分别为 rc1 ,rc2 一直到 rc14 ,排序方式是字母序。 parsePackages 解析出所有的 PKG 均在 matched 中,因为都是模糊匹配,最终传入 bestPackagesFromList 方法中的参数是所有到的 release RPM,预期是返回最新的 release RPM ,即 zbs-5.1.2-rc14,但是返回的是 rc7 。接下来需要调查 bestPackagesFromList 是如何判断的。

['zbs-5.1.2*']
pos: [<YumAvailablePackageSqlite : zbs-5.1.2-rc1.0.release.git.g0cb56434e.el7.SMTX.HCI.x86_64 (0x2bf6e50)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc10.0.release.git.g228f49070.el7.SMTX.HCI.x86_64 (0x2bf6dd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc11.0.release.git.g6d5d763ee.el7.SMTX.HCI.x86_64 (0x2bf6d90)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc12.0.release.git.g9a6400652.el7.SMTX.HCI.x86_64 (0x2bf6bd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc13.0.release.git.g339708733.el7.SMTX.HCI.x86_64 (0x2bf6f90)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc14.0.release.git.g42733ba17.el7.SMTX.HCI.x86_64 (0x2bf6fd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc2.0.release.git.gfa5212d39.el7.SMTX.HCI.x86_64 (0x2c03050)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc3.0.release.git.ge4ecabe7b.el7.SMTX.HCI.x86_64 (0x2c03090)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc4.0.release.git.g6e9ed979b.el7.SMTX.HCI.x86_64 (0x2c030d0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc5.0.release.git.gfa8bab6ad.el7.SMTX.HCI.x86_64 (0x2c03110)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc6.0.release.git.g51f0f1277.el7.SMTX.HCI.x86_64 (0x2c03150)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc7.0.release.git.gccd6dbf2a.el7.SMTX.HCI.x86_64 (0x2c03190)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc8.0.release.git.g763bb9046.el7.SMTX.HCI.x86_64 (0x2c031d0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc9.0.release.git.g238ab2320.el7.SMTX.HCI.x86_64 (0x2c03210)>]
exactmatch: []
matched: [<YumAvailablePackageSqlite : zbs-5.1.2-rc14.0.release.git.g42733ba17.el7.SMTX.HCI.x86_64 (0x2bf6fd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc13.0.release.git.g339708733.el7.SMTX.HCI.x86_64 (0x2bf6f90)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc2.0.release.git.gfa5212d39.el7.SMTX.HCI.x86_64 (0x2c03050)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc7.0.release.git.gccd6dbf2a.el7.SMTX.HCI.x86_64 (0x2c03190)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc3.0.release.git.ge4ecabe7b.el7.SMTX.HCI.x86_64 (0x2c03090)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc9.0.release.git.g238ab2320.el7.SMTX.HCI.x86_64 (0x2c03210)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc6.0.release.git.g51f0f1277.el7.SMTX.HCI.x86_64 (0x2c03150)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc4.0.release.git.g6e9ed979b.el7.SMTX.HCI.x86_64 (0x2c030d0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc10.0.release.git.g228f49070.el7.SMTX.HCI.x86_64 (0x2bf6dd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc8.0.release.git.g763bb9046.el7.SMTX.HCI.x86_64 (0x2c031d0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc12.0.release.git.g9a6400652.el7.SMTX.HCI.x86_64 (0x2bf6bd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc11.0.release.git.g6d5d763ee.el7.SMTX.HCI.x86_64 (0x2bf6d90)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc1.0.release.git.g0cb56434e.el7.SMTX.HCI.x86_64 (0x2bf6e50)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc5.0.release.git.gfa8bab6ad.el7.SMTX.HCI.x86_64 (0x2c03110)>]
unmatched: []
pkgGroups: {'zbs.x86_64': [<YumAvailablePackageSqlite : zbs-5.1.2-rc14.0.release.git.g42733ba17.el7.SMTX.HCI.x86_64 (0x2bf6fd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc13.0.release.git.g339708733.el7.SMTX.HCI.x86_64 (0x2bf6f90)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc2.0.release.git.gfa5212d39.el7.SMTX.HCI.x86_64 (0x2c03050)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc7.0.release.git.gccd6dbf2a.el7.SMTX.HCI.x86_64 (0x2c03190)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc3.0.release.git.ge4ecabe7b.el7.SMTX.HCI.x86_64 (0x2c03090)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc9.0.release.git.g238ab2320.el7.SMTX.HCI.x86_64 (0x2c03210)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc6.0.release.git.g51f0f1277.el7.SMTX.HCI.x86_64 (0x2c03150)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc4.0.release.git.g6e9ed979b.el7.SMTX.HCI.x86_64 (0x2c030d0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc10.0.release.git.g228f49070.el7.SMTX.HCI.x86_64 (0x2bf6dd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc8.0.release.git.g763bb9046.el7.SMTX.HCI.x86_64 (0x2c031d0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc12.0.release.git.g9a6400652.el7.SMTX.HCI.x86_64 (0x2bf6bd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc11.0.release.git.g6d5d763ee.el7.SMTX.HCI.x86_64 (0x2bf6d90)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc1.0.release.git.g0cb56434e.el7.SMTX.HCI.x86_64 (0x2bf6e50)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc5.0.release.git.gfa8bab6ad.el7.SMTX.HCI.x86_64 (0x2c03110)>]}
pkgs: [<YumAvailablePackageSqlite : zbs-5.1.2-rc14.0.release.git.g42733ba17.el7.SMTX.HCI.x86_64 (0x2bf6fd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc13.0.release.git.g339708733.el7.SMTX.HCI.x86_64 (0x2bf6f90)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc2.0.release.git.gfa5212d39.el7.SMTX.HCI.x86_64 (0x2c03050)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc7.0.release.git.gccd6dbf2a.el7.SMTX.HCI.x86_64 (0x2c03190)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc3.0.release.git.ge4ecabe7b.el7.SMTX.HCI.x86_64 (0x2c03090)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc9.0.release.git.g238ab2320.el7.SMTX.HCI.x86_64 (0x2c03210)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc6.0.release.git.g51f0f1277.el7.SMTX.HCI.x86_64 (0x2c03150)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc4.0.release.git.g6e9ed979b.el7.SMTX.HCI.x86_64 (0x2c030d0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc10.0.release.git.g228f49070.el7.SMTX.HCI.x86_64 (0x2bf6dd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc8.0.release.git.g763bb9046.el7.SMTX.HCI.x86_64 (0x2c031d0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc12.0.release.git.g9a6400652.el7.SMTX.HCI.x86_64 (0x2bf6bd0)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc11.0.release.git.g6d5d763ee.el7.SMTX.HCI.x86_64 (0x2bf6d90)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc1.0.release.git.g0cb56434e.el7.SMTX.HCI.x86_64 (0x2bf6e50)>, <YumAvailablePackageSqlite : zbs-5.1.2-rc5.0.release.git.gfa8bab6ad.el7.SMTX.HCI.x86_64 (0x2c03110)>]
toDownload: [<YumAvailablePackageSqlite : zbs-5.1.2-rc7.0.release.git.gccd6dbf2a.el7.SMTX.HCI.x86_64 (0x2c03190)>]

Yum 链接到标题

yumdownloaderyum module 的功能分装,具体的 bestPackagesFromList 是在 yum module 中实现的。bestPackagesFromList 自身先根据 pkg arch 来进行分类,其中判断依据为:

Github: https://github.com/rpm-software-management/yum/blob/master/rpmUtils/arch.py#L153:5

# dict mapping arch -> ( multicompat, best personality, biarch personality )
multilibArches = { "x86_64":  ( "athlon", "x86_64", "athlon" ),
                   "sparc64v": ( "sparcv9v", "sparcv9v", "sparc64v" ),
                   "sparc64": ( "sparcv9", "sparcv9", "sparc64" ),
                   "ppc64":   ( "ppc", "ppc", "ppc64" ),
                   "s390x":   ( "s390", "s390x", "s390" ),
                   }

Github: https://github.com/rpm-software-management/yum/blob/master/yum/__init__.py#L4432

def bestPackagesFromList(self, pkglist, arch=None, single_name=False,
                            req=None):
    """Return the best packages from a list of packages.  This
    function is multilib aware, so that it will not compare
    multilib to singlelib packages.
    :param pkglist: the list of packages to return the best
        packages from
    :param arch: packages will be selected that are compatible
        with the architecture specified by *arch*
    :param single_name: whether to return a single package name
    :param req: the requirement from the user
    :return: a list of the best packages from *pkglist*
    """
    returnlist = []
    compatArchList = self.arch.get_arch_list(arch)
    multiLib = []
    singleLib = []
    noarch = []
    for po in pkglist: # 根据架构来进行筛选,x86_64 是 multiLibArch
        if po.arch not in compatArchList:
            continue
        elif po.arch in ("noarch"):
            noarch.append(po)
        elif isMultiLibArch(arch=po.arch):
            multiLib.append(po) # 最终所有 pkg 添加到 multiLib 中
        else:
            singleLib.append(po)
            
    # we now have three lists.  find the best package(s) of each
    multi = self._bestPackageFromList(multiLib, req=req) # 根据不同架构找到 best package
    single = self._bestPackageFromList(singleLib, req=req)
    no = self._bestPackageFromList(noarch, req=req)

    ...

    return returnlist

继续追踪 _bestPackageFromList 的实现,可以看到

Github: https://github.com/rpm-software-management/yum/blob/master/yum/__init__.py#L4409

def _bestPackageFromList(self, pkglist, req=None):
    """take list of package objects and return the best package object.
        If the list is empty, return None. 
        
        Note: this is not aware of multilib so make sure you're only
        passing it packages of a single arch group.
        :param pkglist: the list of packages to return the best
            packages from
        :param req: the requirement from the user
        :return: a list of the best packages from *pkglist*
    """
    ...
    bestlist = self._compare_providers(pkglist, reqpo=None, req=req)
    return bestlist[0][0]

终于找到最关键的部分: _compare_providers ,这是一个巨大的函数,300行,根据注释可以看到主要用户给 pkg 打分:

Github: https://github.com/rpm-software-management/yum/blob/master/yum/depsolve.py#L1465

def _compare_providers(self, pkgs, reqpo, req=None):
    """take the list of pkgs and score them based on the requesting package
        return a dictionary of po=score"""
    self.verbose_logger.log(logginglevels.DEBUG_4,
            _("Running compare_providers() for %s") %(str(pkgs)))

接下来是具体的打分流程,先根据 repo id 的字母序进行过滤,如果多个 repo 均提供了同一版本的 PKG,那么会根据 repo 字母序进行选取:

    # Do a NameArch filtering, based on repo. __cmp__
    unique_nevra_pkgs = {}
    for pkg in pkgs:
        if (pkg.pkgtup in unique_nevra_pkgs and
            unique_nevra_pkgs[pkg.pkgtup].repo <= pkg.repo):
            continue
        unique_nevra_pkgs[pkg.pkgtup] = pkg
    pkgs = unique_nevra_pkgs.values()

其中 pkg.repo 的实现如下:

class Repository:
    """this is an actual repository object"""       

    def __init__(self, repoid):
        self.id = repoid
        self.quick_enable_disable = {}
        self.disable()
        self._xml2sqlite_local = False

    def __cmp__(self, other):
        """ Sort base class repos. by alphanumeric on their id, also
            see __cmp__ in YumRepository(). """
        if self.id > other.id:
            return 1
        elif self.id < other.id:
            return -1
        else:
            return 0

初始化 pkgresults ,其中 value 是对应的分数,第一步打分是检查目标 pkg 是否已经存在于当前主机上,会通过查询 rpmdb 来获取信息,如果已经存在了,那么这种情况是升级情况,需要与当前主机上 newest 的 pkg 进行比较,如果当前主机上最新的包版本小于 pkg,则 +5 分,如果等于则 +1000 分,如果小于则 -1024 分。如果当前主机上没有,则跳过。

        pkgresults = {}
        penalize = set()

        for pkg in pkgs:
            pkgresults[pkg] = 0 # 初始化各个 pkg 的分数为 0 
        
        # hand this off to our plugins
        self.plugins.run("compare_providers", providers_dict=pkgresults, 
                                      reqpo=reqpo)
        
        for pkg in pkgresults.keys():
            rpmdbpkgs = self.rpmdb.searchNevra(name=pkg.name)
            if rpmdbpkgs:
                #  We only want to count things as "installed" if they are
                # older than what we are comparing, because this then an update
                # so we give preference. If they are newer then obsoletes/etc.
                # could play a part ... this probably needs a better fix.
                newest = sorted(rpmdbpkgs)[-1]
                if newest.verLT(pkg):
                    # give pkgs which are updates just a SLIGHT edge
                    # we should also make sure that any pkg
                    # we are giving an edge to is not obsoleted by
                    # something else in the transaction. :(
                    # there are many ways I hate this - this is but one
                    pkgresults[pkg] += 5
                elif newest.verEQ(pkg):
                    #  We get here from bestPackagesFromList(), give a giant
                    # bump to stuff that is already installed.
                    pkgresults[pkg] += 1000
                elif newest.verGT(pkg):
                    # if the version we're looking at is older than what we have installed
                    # score it down like we would an obsoleted pkg
                    pkgresults[pkg] -= 1024
            else:
                # just b/c they're not installed pkgs doesn't mean they should
                # be ignored entirely. Just not preferred
                pass

O(n^2)遍历 pkgs,先获取 Yum repo 中 newest version,然后保存下来,进行比对,如果当前 pkg 不等于 newest version,则 -1024 分。当前 repo 中匹配的 pkgs 列表一共 14 个, 遍历结束后每个 pkg 的分数应该是 13 * (-1024) = -13312。如果 pkg 被 nextpkg 所废除,那么 pkg 分数继续 -1024。如果传递了 arch 相关参数,那么会根据 arch 进行比较,如果哪个 pkg 提供了当前 arch 的包,那么会 +5 分。

        lpos = {}
        for po in pkgs:
            for nextpo in pkgs:
                if po == nextpo:
                    continue

                #  If this package isn't the latest version of said package,
                # treat it like it's obsoleted. The problem here is X-1
                # accidentally provides FOO, so you release X-2 without the
                # provide, but X-1 is still picked over a real provider.
                if po.name not in lpos:
                    lpos[po.name] = self.pkgSack.returnNewestByName(po.name)[:1]
                if not lpos[po.name] or not po.verEQ(lpos[po.name][0]):
                    pkgresults[po] -= 1024

                obsoleted = False
                if po.obsoletedBy([nextpo]):
                    obsoleted = True
                    pkgresults[po] -= 1024
                                
                    self.verbose_logger.log(logginglevels.DEBUG_4,
                        _("%s obsoletes %s") % (nextpo, po))

                if reqpo:
                    arches = (reqpo.arch, self.arch.bestarch)
                else:
                    arches = (self.arch.bestarch,)
                
                for thisarch in arches:
                    res = _compare_arch_distance(po, nextpo, thisarch)
                    if not res:
                        continue
                    self.verbose_logger.log(logginglevels.DEBUG_4,                   
                       _('archdist compared %s to %s on %s\n  Winner: %s' % (po, nextpo, thisarch, res)))

                    if res == po:
                        pkgresults[po] += 5

            # End of O(N*N): for nextpo in pkgs:

接下来会根据 pkg 是否存在 source rpm,是否是弱引用,是否是直接引用,是否存在冲突等进行分数的增减。

            # End of O(N*N): for nextpo in pkgs:

            # Respect the repository priority for each provider, the default is 80
            pkgresults[po] += (100 - po.repo.compare_providers_priority) * 10
            self.verbose_logger.log(logginglevels.DEBUG_4,
                _('compare_providers_priority for %s is %s' % (po, po.repo.compare_providers_priority)))

            if _common_sourcerpm(po, reqpo):
                self.verbose_logger.log(logginglevels.DEBUG_4,
                    _('common sourcerpm %s and %s' % (po, reqpo)))
                pkgresults[po] += 20
            if _weak_req(po, reqpo):
                self.verbose_logger.log(logginglevels.DEBUG_4,
                    _('weak req %s and %s' % (po, reqpo)))
                pkgresults[po] += 666
            if _info_req(po, reqpo):
                self.verbose_logger.log(logginglevels.DEBUG_4,
                    _('informational req %s and %s' % (po, reqpo)))
                pkgresults[po] += 333
            if _conflict_req(po, reqpo):
                self.verbose_logger.log(logginglevels.DEBUG_4,
                    _('conflict req %s and %s' % (po, reqpo)))
                penalize.add(po)
            if self.isPackageInstalled(po.base_package_name):
                self.verbose_logger.log(logginglevels.DEBUG_4,
                    _('base package %s is installed for %s' % (po.base_package_name, po)))
                pkgresults[po] += 5 # Same as before - - but off of base package name
            if reqpo:
                cpl = _common_prefix_len(po.name, reqpo.name)
                if cpl > 2:
                    self.verbose_logger.log(logginglevels.DEBUG_4,
                        _('common prefix of %s between %s and %s' % (cpl, po, reqpo)))
                
                    pkgresults[po] += cpl*2

当基本分数进行打分完成后, 还存在多个 best pkg,那么会根据当前 OS 安装 pkg 所需依赖数量进行判定,依赖数量越少,则分数越高,最终依赖数量少的 pkg 分数 +1。此时部分 pkg 分数从 -13112 变为 -13111。

        #  If we have more than one "best", see what would happen if we picked
        # each package ... ie. what things do they require that _aren't_ already
        # installed/to-be-installed. In theory this can screw up due to:
        #   pkgA => requires pkgX
        #   pkgB => requires pkgY, requires pkgZ
        # ...but pkgX requires 666 other things. Going recursive is
        # "non-trivial" though, python != prolog. This seems to do "better"
        # from simple testing though.
        bestnum = max(pkgresults.values()) # 将当前 pkg 分数最大的置为 bestnum
        rec_depsolve = {}
        for po in pkgs:
            if pkgresults[po] != bestnum: # 如果当前 pkg 分数不等于最高分,跳过
                continue
            rec_depsolve[po] = 0
        if len(rec_depsolve) > 1: # 如果仍有多个 pkg,则进行依赖判定
            for po in rec_depsolve:
                fake_txmbr = TransactionMember(po)

                #  Note that this is just requirements, so you could also have
                # 4 requires for a single package. This might be fixable, if
                # needed, but given the above it's probably better to leave it
                # like this.
                reqs = self._checkInstall(fake_txmbr) # 检查安装 pkg 所需依赖
                rec_depsolve[po] = len(reqs) # 将依赖数量置为当前 pkg 的分数

            bestnum = min(rec_depsolve.values()) # 找到依赖数量最少的分数作为 bestnum 
            self.verbose_logger.log(logginglevels.DEBUG_4,
                                    _('requires minimal: %d') % bestnum)
            for po in rec_depsolve:
                if rec_depsolve[po] == bestnum: 
                    self.verbose_logger.log(logginglevels.DEBUG_4,
                            _(' Winner: %s') % po)
                    pkgresults[po] += 1 # 将依赖数量结果填充会 pkgresults 中,依赖数量最少的分数 +1
                else:
                    num = rec_depsolve[po]
                    self.verbose_logger.log(logginglevels.DEBUG_4,
                            _(' Loser(with %d): %s') % (num, po))

将当前分数最高的置为 bestnum,遍历 pkgs,如果当前 pkg 分数等于 bestnum,则将其分数 +1000,并将其分数 +(pkg.name)*-1 。如果 -13111 + 1000 +(-3) = -12114。

        #  We don't want to decide to use a "shortest first", if something else
        # has told us to pick something else. But we want to pick between
        # multiple "best" packages. So we spike all the best packages (so
        # only those can win) and then bump them down by package name length.
        bestnum = max(pkgresults.values())
        for po in pkgs:
            if pkgresults[po] != bestnum:
                continue
            pkgresults[po] += 1000
            pkgresults[po] += (len(po.name)*-1)

        # Bump down any packages that we identified as "last-resort" in such a
        # way that they all score below the worst overall score whilst keeping
        # their relative differences.
        shift = max(pkgresults.values()) - min(pkgresults.values()) + 1
        for po in penalize:
            pkgresults[po] -= shift

        bestorder = sorted(pkgresults.items(),
                           key=lambda x: (x[1], x[0]), reverse=True)
        self.verbose_logger.log(logginglevels.DEBUG_4,
                _('Best Order: %s' % str(bestorder)))

        return bestorder

最终根据 pkg 分数进行重新排序,返回第一个结果。

总结 链接到标题

发现这个问题最初是猜测是 Yum Repository 配置问题,阅读代码之后判定是 RPM 在某个版本依赖发生了改变,增加了某些依赖项,导致了 Yum 打分认为其分数较低,从而无法通过 yumdownloader 下载最新的 RPM。与相关同事确认,rc8 版本开始增加了部分依赖,调查结束。