如何在不重做数据库的情况下使用错误的数据库优化此旧Web应用程序

时间:2017-09-08 15:18:02

标签: php database optimization web web-applications

略微修改问题以提高可理解性

我的目标是优化一个Web应用程序,它具有非常糟糕的数据库设计,我无法触及数据库(我无法改变表格,也不会引入新的数据库)。我可以在代码本身,文件系统或代理上操作。 通过"优化"我的意思是:减少发送到webapp的请求,而不是直接发送到文件系统的请求,将数据库查询保持在最低限度,减少不同URL调用的数量(记住缓存)。

让我试着构建一个虚构的例子,只是为了提供一些可供讨论的东西。 让我们想象一下这种情况:

  • 我有一个php webapp,它公开了一个拥有一百万不同人的数据库。
  • ,每个人在某个时刻决定他们是幸福还是悲伤
  • 当我访问person.php?id=x {x=1,..1000000}时,该页面会创建指向show_picture_of_person.php?id=x的链接。 show_picture_of_person.php将进入一百万行的数据库中,这个数据库将通过返回图像告诉我该人是否悲伤或快乐。我不知道这个图像是什么,除非我从数据库中提取它。如果我从数据库中提取它,我可以分析它,并了解它是悲伤的脸还是幸福的脸。函数show_picture_of_person实际上输出图像。 DB也将图像本身存储在blob中。图像总是sad.jpg或happy.jpg。

我希望拥有的内容,而不是show_picture_of_person.php?id=x的一百万个链接,就是拥有2个链接,一个用于sad.jpg,另一个用于happy.jpg。我心中可能的解决方案:

  1. 我编写了一个调用show_picture_of_person所有可能组合的脚本,保存所有图像,了解哪些是相同的,然后编写查找表。我把查找表放在php函数make_sensible_url("show_picture_of_person.php?id=x") -> happy.jpg中。此函数将在person.php脚本中调用。我担心这里的php引擎本身的性能(像这样的数组本身就是一个50 + MB的文件!)
  2. 同上,但不是在PHP中构建数组,而是创建一个包含一百万个文本文件的文件系统,在每个文本文件中,我将获得图像的实际静态文件的名称(避免重复)。函数make_sensible_url("show_picture_of_person.php?id=x")将简单地读取并输出文件的内容。 我喜欢这种方法,因为没有DB调用,读取fs应该比解决方案1中的巨大PHP数组更快。
  3. 我更改了person.php,因此没有指向show_picture_of_person.php的链接,而是我data:images。这个问题是,如果我对person.php进行了x次调用,我仍然会对数据库进行2次调用(一次用于person.php,另一次用于show_picture_of_person.php)。我想只有x调用DB。这也增加了页面的大小(在我的实际情况中,我在1页中有大约20个图像,所以很多查询和大量字节)
  4. 不知道还有什么..
  5. 你会如何解决这个问题?谢谢!

    为了完整性'这是原来的问题:

      

    这是情景:

         
        
    • 一个包含各种数据表的数据库,这些数据没有正确编入索引(对于这个参数,我们说我们有5000个独特的对象,大约有50.000行 - 所以存在重复数据)
    •   
    • 我们处于数据库不可修改的情况下(这也意味着我们无法插入另一个表)
    •   
    • 我们有一个暴露这些对象的php应用程序
    •   
    • 存在大约100万个php调用(全部合法),它们返回5000个对象中的一个(例如:bad_url.php?id = bar,bad_url.php?id = foo,..)
    •   
    • 没有简单的方法来以编程方式决定将返回5000个对象中的哪一个
    •   
         

    我们的目标是以某种方式将百万以上的调用转换为将被调用的调用,这将是giveme.php?id = x,其中x是5000个唯一对象之一。

         

    只是为了让您了解理论方法:

         
        
    • 我们可以索引所有数百万个调用,并使用哪个不同的对象进行映射
    •   
    • 我们可以创建一个哈希表或者其他东西并创建一个php函数,它可以作为give_me_nice_url(" bad_url.php?....")。
    •   
    • 我的感觉是,使用这样的解决方案创建一个阵列会产生一个50-100MB的阵列..不确定它在负载下实时运行的性能如何。
    •   
         

    我对此问题的要求是您将使用哪种方法来解决此问题并处理大型数据集?是否存在比我的解决方案中的查找表更好的方法?请记住,我无法在最终的生产设置中使用数据库。

4 个答案:

答案 0 :(得分:2)

您为每条记录提供文件的选择很简单,但也非常臃肿。如果您知道图像是2个图像之一,则可以大幅减少开销。 假设您使用具有8位字符编码的文件,这8位中的每一个都可以表示happy(设置为1)或sad(设置为0)。这样可以减少8个。因此,对于一百万个条目 - 您的文件将高达125K。

然后是将单个位读取和写入文件的情况。有几个小功能可以做到这一点和一些测试代码...

<?php
error_reporting ( E_ALL );
ini_set ( 'display_errors', 1 );

function getState ( int $user )  {
    $fp = fopen ( "users.dat", "c+" );
    // Find the character in the file (position is user >> 4,
    // which is effectively / 8
    fseek($fp, $user >> 4, SEEK_SET );
    // Read the single char from the file
    $flagByte = fread($fp,1);
    // Extract the bit needed
    //  ord() converts a char to an ascii value ( 0-255)
    //  If the byte hasn't been read - use PHP 7 ?? to set it to a 0
    //  $user & 7 gets the bit position and shifts this bit to position 0
    //  & 1 extracts just this bit
    $flag = (ord($flagByte[0]??chr(0)) >> ($user & 7 )) & 1;
    fclose($fp);
    return $flag;
}

function setState ( int $user, bool $status )  {
    $fp = fopen ( "users.dat", "c+" );
    fseek($fp, $user >> 4, SEEK_SET );
    // Fetch the existing data
    $flagByte = fread($fp,1);
    $flagByte = ord($flagByte[0]??chr(0));
    // Get position of flag
    $flag = 1 << ($user & 7 );
    // Either set or unset the appropriate bit
    if ( $status )  {
        $flagByte |= $flag;
    }
    else    {
        $flagByte &= ~$flag;
    }
    fseek($fp, $user >> 4, SEEK_SET );
    fwrite($fp, chr($flagByte));
    fclose($fp);
}

setState(1, false);
setState(2, true);
setState(3, true);
setState(4, false);
setState(71, true);
setState(600100, false);
setState(600102, true);

echo "User: 1:".getState(1).PHP_EOL;
echo "User: 71:".getState(71).PHP_EOL;
echo "User: 600100:".getState(600100).PHP_EOL;
echo "User: 3:".getState(3).PHP_EOL;
echo "User: 600102:".getState(600102).PHP_EOL;
echo "User: 4:".getState(4).PHP_EOL;
echo "User: 871:".getState(871).PHP_EOL;
echo "User: 3:".getState(3).PHP_EOL;

我确定您可以改进代码。特别是如果它被放入一个类中,您可以打开文件并关闭文件一次而不是每次调用。但如果只有一个记录要进行测试,那么它就不会产生太大的影响。

<强>更新 假设您想要跟踪用户的图像,此方法的添加周期较慢(因为它检查图像是否已经存在),但访问是一个更直接的路径。该概念使用2个文件,一个用于图像名称列表,另一个用于与用户关联的图像。最重要的是当添加新图像时,它会检查图像是否已经存在,如果是,则返回该图像文件中的位置。如果没有找到,它只是将它添加到EOF。所有名称都只是由PHP_EOL终止,因此通过分配固定块对图像名称或浪费的空间没有限制。用户文件只有一个指向这个图像文件的指针,但是(为简单起见)这是一个4字节的无符号整数,因此对于一百万用户来说,这是4MB - 不是那么多。

function imageIndex ( string $addImage ): int {
    $images = fopen ( "images.dat", "c+" );
    while ( true )  {
        $pos = ftell($images);
        $image = fgets($images);
        if ( $image === false || rtrim($image, PHP_EOL) == $addImage )  {
            break;
        }
    }

    if ( $image === false ) {
        fseek($images, 0, SEEK_END);
        $pos = ftell($images);
        fwrite($images, $addImage.PHP_EOL);
    }
    fclose ( $images);
    return $pos;
}

function addUserImage ( int $userID, string $image )    {
    $users = fopen ( "users.dat", "c+" );
    // Fetch image location
    $image = imageIndex($image);
    // Locate user indicator (4 bytes per user)
    $loc = $userID << 2;
    fseek($users, $loc);
    // Write the location as an unsigned integer (4 bytes)
    fwrite($users, pack("L", $image));
    fclose ( $users);
}

function fetchUserImage ( int $userID ): string {
    $users = fopen ( "users.dat", "c+" );
    $images = fopen ( "images.dat", "c+" );
    // Locate user indicator
    $loc = $userID << 2;
    fseek($users, $loc);
    $imgRef = fread($users,4);
    // Convert the 4 chars to a PHP integer
    $imgLoc = unpack("Lloc", $imgRef);
    fseek($images, $imgLoc["loc"]);
    $image = fgets($images);
    fclose ( $users);
    fclose ( $images);

    return rtrim($image,PHP_EOL);
}

// Create 4000 users with some image
// for ( $i=0; $i<2000; $i++ )    {
//     addUserImage($i,"Image{$i}.jpg");
// }
// for ( $i=0; $i<2000; $i++ )    {
//     $ino = 2000 - $i;
//     addUserImage($i+2000,"Image{$ino}.jpg");
// }

// Fetch the image for 2000 users
for ( $i=0; $i < 4000; $i+=2) {
    echo "User {$i} image=".fetchUserImage($i).PHP_EOL;
}

答案 1 :(得分:1)

既然我明白你想做什么,我认为有两个合乎逻辑的案例。

一个案例是你真的只想缓存&#34;快乐/悲伤&#34;逻辑。在这种情况下,我仍然建议使用Varnish或类似的解决方案。

在这种情况下,您的应用输出的HTML不会发生变化(大概是<img src=show_picture_of_person.php?id=x>),并且它会在每个级别进行缓存 - 从浏览器到反向代理。

修改你的PHP代码,为​​来自show_picture_of_person.php?id=x的图像设置一个非常长的TTL - 取决于业务逻辑(人们从快乐到悲伤?你需要使缓存失效吗?),这可能是几小时,几天,几周或几十年。

然后在Varnish中创建一个规则,以便在处理图像请求时尊重该TTL。

这样,只有在缓存中找不到该项时,请求才会访问您的PHP代码。

另一个选择是你想要移动逻辑&#34;是人x快乐或悲伤&#34;远离通过昂贵的数据库调用检索图像,而是将其烘焙到其他数据存储机制中,而HTML代码变为<img src=happy.jpg>

由于您无法将其存储在数据库中,因此您需要查看其他选项 - 将其有效地存储在文件系统中,可以是一个巨大的数组,也可以是很多小文件。

这有几个问题 - 存储数据的最佳位置是在数据库中;将其分离到不同的位置会产生维护和可理解性的挑战。一个巨大的阵列并没有真正扩展。存储和访问大量小文件很慢,有些文件系统limit the number of files in a directory或速度超过一定限制。

将数据作为文件存储在硬盘驱动器上的最大挑战是,从可扩展性的角度来看,您的应用程序将成为I / O绑定。很难说这是否是一个真正的问题 - 我使用像Apache JMeter这样的东西运行一些相当严格的可扩展性测试。

我的建议是使用众多off-the-shelf PHP caching solutions中的一个,而不是自己烤。哪一个取决于您可以在您的环境中安装什么。

答案 2 :(得分:1)

我会将show_picture_of_person.php?id=x的结果缓存到文件系统,这与您的方法#2类似。

但是,您可能需要考虑使用缓存库而不是自己编辑。某些框架(如laravel)附带缓存选项,或者您可以使用第三方缓存库,例如https://github.com/tedious/Stash

以下是藏匿的示例:

// show_picture_of_person.php

if (!isset($_GET['image_path'])) {
    // Create Driver with default options
    $driver = new Stash\Driver\FileSystem(array());
    // Inject the driver into a new Pool object.
    $pool = new Stash\Pool($driver);
    // Get a cache item.
    $item = $pool->getItem('image_path_' . $_GET['id']);

    // Attempt to get the data
    $image_path = $item->get();

    // Check to see if the data was a miss.
    if($item->isMiss())
    {
        // Let other processes know that this one is rebuilding the data.
        $item->lock();

        // Run intensive code
        $image_path = codeThatTakesALongTime(); // save image to disk here

        if ($image_path) {
            // Store the expensive to generate data.
            $pool->save($item->set($image_path));
        }
    }
}
else {
    $image_path = $_GET['image_path'];
}

// Continue as normal.
useDataForStuff($image_path);

person.php?id=x中,您现在可以检查上面的缓存中的密钥x,如果已填充show_picture_of_person.php?image_path=[sad/happy].jpg,如果它未填充,则渲染{ {1}},一旦点击就会填充缓存。

答案 3 :(得分:0)

在某些结构化查询语言方言中,存在“CURSOR”的概念。类似于每页返回10或50个搜索结果。

如果你使用它,那么你可以查询在页面上显示的合适的,甚至可选择的,大小的行范围,并且你也可以计算相同行数的任意范围,以显示链接列表浏览下一个或上一个结果。

计算此类网页的总数更为昂贵。