确定最近的镜像 PHP

8
我维护一个包含可下载文件的网站。目前它托管在美国的服务器上,但我最近在德国获得了一台新服务器。我想将下载内容镜像到德国的服务器,并在第一个服务器上(托管网站的服务器)使用PHP脚本根据用户的位置检测应该使用哪个文件镜像。例如,如果用户在加拿大,则应从我当前在美国的服务器下载文件。如果他们在法国,则应从德国获取文件,而不是通过大西洋进行下载。那么,我如何确定他们更接近哪个国家呢?
我知道MaxMind GeoIP,并已经安装了它,但它只给我一个国家,据我所知,没有办法自动确定给定国家最接近我的两个镜像国家中的哪一个。我想我可以按照大陆来做:让亚洲、欧洲、非洲和澳大利亚的用户从德国获取内容,让北美和南美的访问者从美国获取文件。如果有人能想到更好的解决方案,我愿意听取建议。
嗯,我想我会采用按大陆分类的原始想法。对于其他想要做这种事情的人来说,这将是一个不错的起点。当我在欧洲有多个镜像时,问题就会出现,但大陆的想法现在必须起作用。

@Jonasm:不错的想法,但我认为你不能仅仅使用地图API来计算距离,你实际上必须显示一个地图,在这种情况下肯定是不令人满意的。 - Ashley Strout
你是否考虑过使用亚马逊云服务、Akamai或其他众多内容分发网络,而不是自己创建镜像?它们会为你处理所有这些问题。 - bumperbox
1
@bumperbox 在我的情况下这是不可行的。对于那些负担得起的人来说,这是一个好主意,但我在非常有限的预算内运营这个网站。实际上,我完全可以不用位置感知镜像系统,但我觉得这应该是可行的,我希望这个问题(以及希望得到一个好答案)能够被需要做这个的人所参考。这似乎是一个相当普遍的情况。 - Ashley Strout
1
使用任播 DNS 是实现这一目标的最佳方式,简而言之,所有服务器都获得相同的 IP 地址,用户可以无缝地路由到最近的服务器,而无需编写任何代码。这样做的优点是,如果一个服务器出现故障,您还可以获得备份保护,并且可以添加新的服务器,而不必每次都担心应用程序的大量更改。 - Anigel
PHP.net提供按国家选择/提供镜像,http://www.php.net/source.php?url=/include/ip-to-country.inc - hakre
显示剩余4条评论
7个回答

2

目前提出的解决方案中似乎存在很多开发人员的额外工作量。如果这是我自己应用程序中需要解决的问题,我可能会选择重新发明轮子,从而节省一些工作时间。

确定最接近的镜像(使用邮政编码)

  1. 在数组中维护可用镜像服务器的邮政编码列表。
  2. 确定用户代理的邮政编码(例如用户输入或PHP库)。
  3. 计算两个邮政编码之间的距离(例如PHP库)。
  4. 根据返回的距离选择镜像。

请记住,较短的距离并不一定意味着响应时间更快。然而,在您的情况下,一个国家内的镜像显然比另一个国家的镜像更快,假设两个镜像都正常运行。继续阅读以获取我认为更为“健壮”的解决方案。

资源和链接

“Maverick”方法

在我看来,Mavericks也被称为那些创新者、问题解决者和发明家,他们开发了我们今天都在使用的伟大库和框架。有时会错误地与“hackish”思想联系起来,但我们欣然接受这个称号 :)

  1. 在任何一个镜像服务器上创建您自己的API服务,该服务将接受$_GET或$_POST请求。

  2. 该API服务将获取所提供的IP地址并使用ping()进行计算响应时间,然后取平均值,并将其返回到请求接口(例如连接客户端和/或试图确定最近的镜像的前端门户)。响应平均值最低的服务器应该是您最快响应的服务器,但不一定是最近的。哪个对你更重要?有关可以使用不依赖于本地执行shell命令(例如跨平台)的工作ping()函数,请参见Ping site and return result in PHP

  3. 最后一步:在后台将请求客户端的IP地址传递给任一镜像服务器上运行的API服务。我们都知道如何派生IP地址,但并不如你认为的那样好。如果您进行负载平衡或在代理后面,则可能需要首先检查是否通过了任何这些标头(HTTP_FORWARDED,HTTP_FORWARDED_FOR,HTTP_X_FORWARDED,HTTP_X_FORWARDED_FOR,HTTP_CLIENT_IP)。如果是这样,则这可能是用户代理的真实 IP地址。

在此时(第3步),当每个镜像去ping用户代理时,您将比较所回复的平均响应时间。然后根据选择用户代理应该从哪个镜像下载。您将创建的服务流程类似于以下内容:

  1. 用户代理访问门户
  2. 门户使用后台AJAX/jQuery请求(或传统的POST和重定向)将用户代理的IP地址转发到分别在两个镜像上运行的API服务。
  3. 运行在镜像上的API服务对接收到的IP地址进行ping()计算,并返回所配置的总响应数的平均值。
  4. 门户读取返回的平均值并进行比较。

希望这可以帮助您愉快编码!


1
这是我迄今为止看到的第一个真正好且易于实现的想法。我非常喜欢它。有一个问题:这会导致多少开销?考虑到每天大约500个访问者,这堆小请求最终会对服务器造成困扰吗? - Ashley Strout
PHP绝对有能力做到以上及以上,因此由程序员来基准测试和创建测试单元以评估其性能。关于您的担忧,一个可能的解决方案是提出缓存解决方案,其中服务器选择已经预先确定。确定用户代理IP地址的网络部分,并将服务器分配给该网络(例如192.168.0.0)。然后,如果传入的IP地址与数据库中的网络部分匹配,则您的门户可以跳过执行API调用并直接跳转到服务器配置。 - Matt Borja
另外,还有一件事需要指出 - 有些人配置他们的路由器不回复ICMP ping,而其他人仍然没有可用的端口连接进行测试(根据我帖子中的ping()参考)。完全有可能会得到请求超时的情况。在这种情况下,我建议更新您的API服务,以回退到IP到邮政编码距离计算函数,该函数返回数字形式的距离,而不是返回平均ping响应作为数字形式。再次强调,理解距离并不一定意味着更快的连接。 - Matt Borja

1
如果您只有两个镜像,请在浏览器中启动AJAX请求,从每个服务器下载一个50K文件。这个文件足够小,不会对用户造成巨大的延迟,但足够大,使计时器测量差异显著 - 当然,您应该稍微调整一下这个数字。
然后,一旦您得到了“最佳时间”,设置JS cookie并在需要下载时重定向到首选镜像。可以从后台的下载页面启动测量,因此用户可能不会注意到延迟(当他们选择所需的文件时)。
您甚至可以在每个AJAX操作中回复“服务器负载”,并根据当前负载而不仅仅是响应时间选择最佳服务器。因此,即使最近的服务器在德国,如果后者的负载显着高于前者,则英国用户也将使用美国服务器。

我实际上很喜欢这个想法。 - Someone

0

执行一个 traceroute(配置 traceroute 客户端不解析主机名和设置较短的超时时间)。

根据跳数和 traceroute 客户端的位置(我认为与 PHP 脚本相同),在美国和德国之间选择。

地理距离与网络距离、网络速度或带宽成本无关。

作为 traceroute 的替代方案(因为它是一个 hackish,小型代码解决方案),我建议您使用 $_SERVER["REMOTE_ADDR"] 并在 geo ip database 中查找以获取国家代码。 如果国家代码不是美洲国家之一,则为避免穿越拥挤的互联网骨干,回退到德国(此外,您可以将国家代码条件为来自欧洲)。

设置好 geo ip 数据库后,我建议您将 IP 地址从点分格式转换为整数格式,以获得速度和查询的便利性。

根据我对上述 geo ip 数据库的经验,它很少缺失,所以并不重要。


0

你觉得使用像geoip这样的库,然后使用纬度和经度来比较镜像和用户之间的距离不是更容易吗?

我认为这样做更简单,更易于实现,适用于N个镜像,并且您不需要请求Zip或其他类型的数据来进行参考。


0

我不记得有任何库可以做到这一点。但是,如果我有一个想法,可能能够帮助你。

使用此距离计算器计算两个IP地址之间的距离。或者找出两个IP地址(一个服务器和一个客户端)的纬度和经度,并计算它们之间的距离。以下是一个伪代码示例:

distance = ( 3956 *2 * ASIN( SQRT( POWER( SIN( ( 34.1012181 - ABS( latitude ) ) * PI( ) /180 /2 ) , 2 ) + COS( 34.1012181 * PI( ) /180 ) * COS( ABS( latitude ) * PI( ) /180 ) * POWER( SIN( ( ABS( - 118.325739 ) - ABS( longitude ) ) * PI( ) /180 /2 ) , 2 ) ) ))

0

抱歉:我来晚了。:-)

通过访问Maxmind GeoLite2 City Database,您可以获取镜像和访问者的纬度和经度,并进行相应的重定向。

我创建了一个小脚本,根据最近的服务器告诉您应该重定向到哪个服务器。您只需执行header("Location: .....); 重定向即可,而不是回显语句。

这是完整的示例:

<?php
/**
 * Credit for the function: https://dev59.com/WGkw5IYBdhLWcg3wNX73#10054282
 * Calculates the great-circle distance between two points, with
 * the Vincenty formula.
 * @param float $latitudeFrom Latitude of start point in [deg decimal]
 * @param float $longitudeFrom Longitude of start point in [deg decimal]
 * @param float $latitudeTo Latitude of target point in [deg decimal]
 * @param float $longitudeTo Longitude of target point in [deg decimal]
 * @param float $earthRadius Mean earth radius in [m]
 * @return float Distance between points in [m] (same as earthRadius)
 */
function vincentyGreatCircleDistance(
  $latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo, $earthRadius = 6371000)
{
  // convert from degrees to radians
  $latFrom = deg2rad($latitudeFrom);
  $lonFrom = deg2rad($longitudeFrom);
  $latTo = deg2rad($latitudeTo);
  $lonTo = deg2rad($longitudeTo);

  $lonDelta = $lonTo - $lonFrom;
  $a = pow(cos($latTo) * sin($lonDelta), 2) +
    pow(cos($latFrom) * sin($latTo) - sin($latFrom) * cos($latTo) * cos($lonDelta), 2);
  $b = sin($latFrom) * sin($latTo) + cos($latFrom) * cos($latTo) * cos($lonDelta);

  $angle = atan2(sqrt($a), $b);
  return $angle * $earthRadius;
}

$download_servers = array(      0 => array(     'hostname'  => "ftp.bu.edu",
                                                'longitude' => null,
                                                'latitude'  => null,
                                                'city'      => null,
                                                'distance'  => null),
                                1 => array(     'hostname'  => "www.softlayer.com",
                                                'longitude' => null,
                                                'latitude'  => null,
                                                'city'      => null,
                                                'distance'  => null),
                                2 => array(     'hostname'  => "download.nust.na",
                                                'longitude' => null,
                                                'latitude'  => null,
                                                'city'      => null,
                                                'distance'  => null),
                                3 => array(     'hostname'  => "mirror.isoc.org.il",
                                                'longitude' => null,
                                                'latitude'  => null,
                                                'city'      => null,
                                                'distance'  => null),
                                4 => array(     'hostname'  => "download.nus.edu.sg",
                                                'longitude' => null,
                                                'latitude'  => null,
                                                'city'      => null,
                                                'distance'  => null),
                                5 => array(     'hostname'  => "mirror.yandex.ru",
                                                'longitude' => null,
                                                'latitude'  => null,
                                                'city'      => null,
                                                'distance'  => null),
                                6 => array(     'hostname'  => "ftp.iij.ad.jp",
                                                'longitude' => null,
                                                'latitude'  => null,
                                                'city'      => null,
                                                'distance'  => null)
                          );

require_once( __DIR__ . "/maxmind/geoip2.phar");
use GeoIp2\Database\Reader;

// City DB
$reader = new Reader( __DIR__ . '/maxmind/GeoLite2-City.mmdb');
$record = $reader->city($_SERVER['REMOTE_ADDR']);

$i = 0;
foreach ($download_servers as $server) {
   //$record2 = $reader->city($server['ip']);
   $record2 = $reader->city(gethostbyname($server['hostname']));
   if (!isset($download_servers[$i]['longitude'])) {
      $download_servers[$i]['longitude'] = $record2->location->longitude;
      $download_servers[$i]['latitude'] = $record2->location->latitude;
   }
   $download_servers[$i]['city'] = $record2->city->name;
   $download_servers[$i]['distance'] = vincentyGreatCircleDistance(     $record->location->latitude, $record->location->longitude,
                                                                        $download_servers[$i]['latitude'], $download_servers[$i]['longitude']);
   $i++;
}

$dists = array_column($download_servers, 'distance');
$min = array_search(min($dists), $dists, true);

echo "Nearest download server is: " . $download_servers[$min]['hostname'] . " in " . $download_servers[$min]['city']  . "<br>";
echo "Distance to you (" . $record->city->name . ") is: " . $download_servers[$min]['distance'] / 1000 . " km";

0
我来晚了,但是最简单的解决方案不就是同时ping两个并看哪个更快吗?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接