2008/07/22

ufs의 disk quota 동작

커널 모듈인 ufs는 화일 시스템에 쓰기 행위가 발생할 때 disk quota를 확인하는 과정을 밟습니다. 이러한 과정은 quotaon/off와는 관계없이 실행되는데, quota on이 되어 있으면 많은 것을 실행하는 것이고, quotaoff인 경우에는 빠르게 리턴하는 것이죠.

다음은 ufs_write시 일어나는 ufs 모듈내의 흐름입니다. 더 하부 모듈인 genunix, unix와 관련된 다른 모듈은 캡쳐하지 않았습니다. dtrace로 캡쳐했으며 캡쳐한 방법은 다음과 같습니다.

#dtrace -F -n 'io:::start/uid==31523 && args[1]->dev_statname =="cmdk0"/{trace(timestamp);self->trace=1;}' \
-n 'fbt:ufs:ufs_write:entry/self->trace/{trace(timestamp);self->ufs=1;}' \
-n 'fbt:ufs::/self->trace&&self->ufs/{trace(timestamp);}' \
-n 'fbt:ufs:ufs_write:return/self->trace&&self->ufs/ {trace(timestamp);self->trace=0;self->ufs=0;exit(1);}'



결과는 아래 그림과 같다.

그림에서 보듯이, ufs_write가 발생할때 chkdq라는 것을 지나가는데, chkdq()는 아래와 같은 내용을 가진다.
ufs quota는 inode quota와 data quota를 동시에 가지는데, 아래는 ufs_write()를 통해서 data block의 write가 일어난 경우를 잡은 경우이기 때문에 data quota만 체크한다.

그런데, 중요한것은 chkdq()에서 무엇을 하느냐 인데, 아래 코드에서 볼 수 있는 바와 같이,

<br /><br /><span style="font-family: courier new;font-size:85%;" >124 int<br />125 chkdq(struct inode *ip, long change, int force, struct cred *cr,<br />126         char **uerrp, size_t *lenp)<br />127 {<br />128         struct dquot *dqp;<br />129         uint64_t ncurblocks;<br />130         struct ufsvfs *ufsvfsp = ip->i_ufsvfs;<br />131         int error = 0;<br />132         long abs_change;<br />133         char *msg1 =<br />134 "!quota_ufs: over hard disk limit (pid %d, uid %d, inum %d, fs %s)\n";<br />135         char *msg2 =<br />136 "!quota_ufs: Warning: over disk limit (pid %d, uid %d, inum %d, fs %s)\n";<br />137         char *msg3 =<br />138 "!quota_ufs: over disk and time limit (pid %d, uid %d, inum %d, fs %s)\n";<br />139         char *msg4 =<br />140 "!quota_ufs: Warning: quota overflow (pid %d, uid %d, inum %d, fs %s)\n";<br />141         char *errmsg = NULL;<br />142         time_t now;<br />143<br />144         /*<br />145          * Shadow inodes do not need to hold the vfs_dqrwlock lock.<br />146          */<br />147         ASSERT((ip->i_mode & IFMT) == IFSHAD ||<br />148             RW_LOCK_HELD(&ufsvfsp->vfs_dqrwlock));<br />149         ASSERT(RW_WRITE_HELD(&ip->i_contents));<br />150<br />151         if (change == 0)<br />152                 return (0);<br />153         dqp = ip->i_dquot;<br />154<br />155         /*<br />156          * Make sure the quota info record matches the owner.<br />157          */<br />158         ASSERT(dqp == NULL || ip->i_uid == dqp->dq_uid);<br />159<br />160 #ifdef DEBUG<br />161         /*<br />162          * Shadow inodes and extended attribute directories<br />163          * should not have quota info records.<br />164          */<br />165         if ((ip->i_mode & IFMT) == IFSHAD || (ip->i_mode & IFMT) == IFATTRDIR) {<br />166                 ASSERT(dqp == NULL);<br />167         }<br />168         /*<br />169          * Paranoia for verifying that quotas are okay.<br />170          */<br />171         else {<br />172                 struct dquot *expect_dq;<br />173                 int mismatch_ok = 0;<br />174<br />175                 /* Get current quota information */<br />176                 expect_dq = getinoquota(ip);<br />177                 /*<br />178                  * We got NULL back from getinoquota(), but there is<br />179                  * no error code return from that interface and some<br />180                  * errors are "ok" because we may be testing via error<br />181                  * injection.  If this is not the quota inode then we<br />182                  * use getdiskquota() to see if there is an error and<br />183                  * if the error is ok.<br />184                  */<br />185                 if (expect_dq == NULL && ip != ufsvfsp->vfs_qinod) {<br />186                         int error;<br />187                         struct dquot *xdqp;<br />188<br />189                         error = getdiskquota((uid_t)ip->i_uid, ufsvfsp, 0,<br />190                             &xdqp);<br />191                         switch (error) {<br />192                         /*<br />193                          * Either the error was transient or the quota<br />194                          * info record has no limits which gets optimized<br />195                          * out by getinoquota().<br />196                          */<br />197                         case 0:<br />198                                 if (xdqp->dq_fhardlimit == 0 &&<br />199                                     xdqp->dq_fsoftlimit == 0 &&<br />200                                     xdqp->dq_bhardlimit == 0 &&<br />201                                     xdqp->dq_bsoftlimit == 0) {<br />202                                         mutex_enter(&xdqp->dq_lock);<br />203                                         dqput(xdqp);<br />204                                         mutex_exit(&xdqp->dq_lock);<br />205                                 } else {<br />206                                         expect_dq = xdqp;<br />207                                 }<br />208                                 break;<br />209<br />210                         case ESRCH:     /* quotas are not enabled */<br />211                         case EINVAL:    /* error flag set on cached record */<br />212                         case EUSERS:    /* quota table is full */<br />213                         case EIO:       /* I/O error */<br />214                                 mismatch_ok = 1;<br />215                                 break;<br />216                         }<br />217                 }<br />218<br />219                 /*<br />220                  * Make sure dqp and the current quota info agree.<br />221                  * The first part of the #ifndef is the quick way to<br />222                  * do the check and should be part of the standard<br />223                  * DEBUG code. The #else part is useful if you are<br />224                  * actually chasing an inconsistency and don't want<br />225                  * to have to look at stack frames to figure which<br />226                  * variable has what value.<br />227                  */<br />228 #ifndef CHASE_QUOTA<br />229                 ASSERT(mismatch_ok || dqp == expect_dq);<br />230 #else /* CHASE_QUOTA */<br />231                 if (expect_dq == NULL) {<br />232                         /*<br />233                          * If you hit this ASSERT() you know that quota<br />234                          * subsystem does not expect quota info for this<br />235                          * inode, but the inode has it.<br />236                          */<br />237                         ASSERT(mismatch_ok || dqp == NULL);<br />238                 } else {<br />239                         /*<br />240                          * If you hit this ASSERT() you know that quota<br />241                          * subsystem expects quota info for this inode,<br />242                          * but the inode does not have it.<br />243                          */<br />244                         ASSERT(dqp);<br />245                         /*<br />246                          * If you hit this ASSERT() you know that quota<br />247                          * subsystem expects quota info for this inode<br />248                          * and the inode has quota info, but the two<br />249                          * quota info pointers are not the same.<br />250                          */<br />251                         ASSERT(dqp == expect_dq);<br />252                 }<br />253 #endif /* !CHASE_QUOTA */<br />254                 /*<br />255                  * Release for getinoquota() above or getdiskquota()<br />256                  * call when error is transient.<br />257                  */<br />258                 if (expect_dq) {<br />259                         mutex_enter(&expect_dq->dq_lock);<br />260                         dqput(expect_dq);<br />261                         mutex_exit(&expect_dq->dq_lock);<br />262                 }<br />263         }<br />264 #endif /* DEBUG */<br />265<br />266         /*<br />267          * Shadow inodes and extended attribute directories<br />268          * do not have quota info records.<br />269          */<br />270         if (dqp == NULL)<br />271                 return (0);<br />272         /*<br />273          * Quotas are not enabled on this file system so there is nothing<br />274          * more to do.<br />275          */<br />276         if ((ufsvfsp->vfs_qflags & MQ_ENABLED) == 0) {<br />277                 return (0);<br />278         }<br />279         mutex_enter(&dqp->dq_lock);<br />280         if (change <>dq_flags |= DQ_MOD;<br />282                 abs_change = -change;   /* abs_change must be positive */<br />283                 if (dqp->dq_curblocks <>dq_curblocks = 0;<br />285                 else<br />286                         dqp->dq_curblocks += change;<br />287                 if (dqp->dq_curblocks <>dq_bsoftlimit)<br />288                         dqp->dq_btimelimit = 0;<br />289                 dqp->dq_flags &= ~DQ_BLKS;<br />290                 TRANS_QUOTA(dqp);<br />291                 mutex_exit(&dqp->dq_lock);<br />292                 return (0);<br />293         }<br />294<br />295         /*<br />296          * Adding 'change' to dq_curblocks could cause an overflow.<br />297          * So store the result in a 64-bit variable and check for<br />298          * overflow below.<br />299          */<br />300         ncurblocks = (uint64_t)dqp->dq_curblocks + change;<br />301<br />302         /*<br />303          * Allocation. Check hard and soft limits.<br />304          * Skip checks for uid 0 owned files.<br />305          * This check used to require both euid and ip->i_uid<br />306          * to be 0; but there are no quotas for uid 0 so<br />307          * it really doesn't matter who is writing to the<br />308          * root owned file.  And even root cannot write<br />309          * past a user's quota limit.<br />310          */<br />311         if (ip->i_uid == 0)<br />312                 goto out;<br />313<br />314         /*<br />315          * Disallow allocation if it would bring the current usage over<br />316          * the hard limit or if the user is over his soft limit and his time<br />317          * has run out.<br />318          */<br />319         if (dqp->dq_bhardlimit && ncurblocks >= (uint64_t)dqp->dq_bhardlimit &&<br />320             !force) {<br />321                 /* If the user was not informed yet and the caller      */<br />322                 /* is the owner of the file                             */<br />323                 if ((dqp->dq_flags & DQ_BLKS) == 0 &&<br />324                     ip->i_uid == crgetruid(cr)) {<br />325                         errmsg = msg1;<br />326                         dqp->dq_flags |= DQ_BLKS;<br />327                 }<br />328                 error = EDQUOT;<br />329                 goto out;<br />330         }<br />331         if (dqp->dq_bsoftlimit && ncurblocks >= (uint64_t)dqp->dq_bsoftlimit) {<br />332                 now = gethrestime_sec();<br />333                 if (dqp->dq_curblocks <>dq_bsoftlimit ||<br />334                     dqp->dq_btimelimit == 0) {<br />335                         dqp->dq_flags |= DQ_MOD;<br />336                         dqp->dq_btimelimit = now +<br />337                             ((struct ufsvfs *)ITOV(ip)->v_vfsp->vfs_data)<br />338                             ->vfs_btimelimit;<br />339                         if (ip->i_uid == crgetruid(cr)) {<br />340                                 errmsg = msg2;<br />341                         }<br />342                 } else if (now > dqp->dq_btimelimit && !force) {<br />343                         /* If the user was not informed yet and the     */<br />344                         /* caller is the owner of the file              */<br />345                         if ((dqp->dq_flags & DQ_BLKS) == 0 &&<br />346                             ip->i_uid == crgetruid(cr)) {<br />347                                 errmsg = msg3;<br />348                                 dqp->dq_flags |= DQ_BLKS;<br />349                         }<br />350                         error = EDQUOT;<br />351                 }<br />352         }<br />353 out:<br />354         if (error == 0) {<br />355                 dqp->dq_flags |= DQ_MOD;<br />356                 /*<br />357                  * ncurblocks can be bigger than the maximum<br />358                  * number that can be represented in 32-bits.<br />359                  * When copying ncurblocks to dq_curblocks<br />360                  * (an unsigned 32-bit quantity), make sure there<br />361                  * is no overflow.  The only way this can happen<br />362                  * is if "force" is set.  Otherwise, this allocation<br />363                  * would have exceeded the hard limit check above<br />364                  * (since the hard limit is a 32-bit quantity).<br />365                  */<br />366                 if (ncurblocks > 0xffffffffLL) {<br />367                         dqp->dq_curblocks = 0xffffffff;<br />368                         errmsg = msg4;<br />369                 } else {<br />370                         dqp->dq_curblocks = ncurblocks;<br />371                 }<br />372         }<br />373<br />374         if (dqp->dq_flags & DQ_MOD)<br />375                 TRANS_QUOTA(dqp);<br />376<br />377         mutex_exit(&dqp->dq_lock);<br />378         /*<br />379          * Check for any error messages to be sent<br />380          */<br />381         if (errmsg != NULL) {<br />382                 /*<br />383                  * Send message to the error log.<br />384                  */<br />385                 if (uerrp != NULL) {<br />386                         /*<br />387                          * Set up message caller should send to user;<br />388                          * gets copied to the message buffer as a side-<br />389                          * effect of the caller's uprintf().<br />390                          */<br />391                         *lenp = strlen(errmsg) + 20 + 20 +<br />392                             strlen(ip->i_fs->fs_fsmnt) + 1;<br />393                         *uerrp = (char *)kmem_alloc(*lenp, KM_NOSLEEP);<br />394                         if (*uerrp != NULL) {<br />395                                 /* errmsg+1 => skip leading ! */<br />396                                 (void) sprintf(*uerrp, errmsg+1,<br />397                                     (int)ttoproc(curthread)->p_pid,<br />398                                     (int)ip->i_uid, (int)ip->i_number,<br />399                                     ip->i_fs->fs_fsmnt);<br />400                         }<br />401                 } else {<br />402                         /*<br />403                          * Caller doesn't care, so just copy to the<br />404                          * message buffer.<br />405                          */<br />406                         cmn_err(CE_NOTE, errmsg,<br />407                             (int)ttoproc(curthread)->p_pid,<br />408                             (int)ip->i_uid, (int)ip->i_number,<br />409                             ip->i_fs->fs_fsmnt);<br />410                 }<br />411         }<br />412         return (error);<br />413 }<br /></span></pre><span style="font-family: courier new;font-size:100%;" >흥미로운 것은 솔라리스 10에 새로이 생긴 zfs에서는 어떻게 quota를 사용할까 ?<br />zfs는 metadata에 대한 quota를 사용하지 않는다, 기본적으로 화일의 수를 할당된 공간과 별도로 체크하는 것이 의미가 없기 때문이다. 따라서, 또한, zfs는 dataset layer에서 블럭을 할당할때마다 used byte가 할당이 되며, 모든 블럭은 객체지향적으로 상속되므로, uber block은 하위 블럭의 총합에 대한 것을 늘 유지하게 되므로 quota check하는 것이 매우 단순하다.<br /><br /></span><pre><span style="font-family: courier new;font-size:85%;" ><span style="font-size:100%;">아래가 dataset에서 quota를 check하는 모듈이다. 매우 단순함을 알 수 있다.<br />코드의 길이만 봐도 ufs의 경우에는 300라인에 해당하지만, zfs의 경우에는<br />40여라인밖에 되지 않는다. 아래의 모듈에서는 위 ufs용 모듈인 chkdq()와는 달리<br />사용자 id를 기준으로 비교하는 라인도 없다.<br /><br />코드의 길이가 중요한 이유는 data를 쓰기하는 경우에, 솔라리스는 mutex_lock()을 설정하는데<br />코드의 길이가 길게 되며, lock이 잡히는 시간이 증가하게 되므로, 병목의 발생 가능성이 증가하게<br />된다.<br /><br />따라서, zfs는 quota() 기능 사용에 따른 오버헤드가 매우 작을 수 있음을 예측할 수 있다.<br />반면, 이런 오해가 있을 수도 있다. ufs는 단일 화일 시스템을 여러 사용자를 기준으로 사용 공간을<br />나누어 줄 수 있는데, zfs는 불가능한 것이 아닌가?<br /><br />그러나, 이런 오해는 ufs와 zfs의 근본적인 차이를 이해하지 못해서 발생하는 것이다. zfs는 모든 것이<br />pool에 기반하기 때문에 사용자별로 별개의 zfs를 만들어서 할당할 수 있는 볼륨기능이 통합되어 있다.<br />따라서, 단일 화일시스템을 사용자별로 쪼개고 붙이고할 필요가 없다. 그냥 필요한 공간만큼 할당만<br />하면 된다.  혹여나 모 사용자에게 너무 많이 할당했다면, 반납하고 필요한 사용자에게 할당하면 된다.<br /><br />반면, ufs는 볼륨 매니저에서 사용자별로 쪼개놓지 않으면 하나의 단일 화일 시스템에서 여러 사용자용<br />쿼타를 설정해야하므로 매우 불편할 뿐 아니라, 다른 화일 시스템의 사용자에게 남는 공간을 떼어줄 수도<br />없다.<br /><br />어떤 화일 시스템을 사용해야 하는가? 관리의 포인트를 중히 여긴다면 선택의 여지가 없다. 두말할 필요없이<br />zfs라고 할 수 있겠다. zfs 기반으로 서비스를 제공하는 순간 많은 사람들이 편하게 된다는 점을 염두에<br />두어야 한다. 서비스 업체들은 자사 엔지니어가 스토리지 관리때문에 고객들에게 뺏기는 시간이 혁신적으로<br />짧아진다는 것을 알아야한다. 왜냐하면, 시간은 곧 돈이기 때문이다.</span><br /><br /><br /><br />2736 int<br />2737 dsl_dataset_check_quota(dsl_dataset_t *ds, boolean_t check_quota,<br />2738     uint64_t asize, uint64_t inflight, uint64_t *used, uint64_t *ref_rsrv)<br />2739 {<br />2740         int error = 0;<br />2741<br />2742         ASSERT3S(asize, >, 0);<br />2743<br />2744         /*<br />2745          * *ref_rsrv is the portion of asize that will come from any<br />2746          * unconsumed refreservation space.<br />2747          */<br />2748         *ref_rsrv = 0;<br />2749<br />2750         mutex_enter(&ds->ds_lock);<br />2751         /*<br />2752          * Make a space adjustment for reserved bytes.<br />2753          */<br />2754         if (ds->ds_reserved > ds->ds_phys->ds_unique_bytes) {<br />2755                 ASSERT3U(*used, >=,<br />2756                     ds->ds_reserved - ds->ds_phys->ds_unique_bytes);<br />2757                 *used -= (ds->ds_reserved - ds->ds_phys->ds_unique_bytes);<br />2758                 *ref_rsrv =<br />2759                     asize - MIN(asize, parent_delta(ds, asize + inflight));<br />2760         }<br />2761<br />2762         if (!check_quota || ds->ds_quota == 0) {<br />2763                 mutex_exit(&ds->ds_lock);<br />2764                 return (0);<br />2765         }<br />2766         /*<br />2767          * If they are requesting more space, and our current estimate<br />2768          * is over quota, they get to try again unless the actual<br />2769          * on-disk is over quota and there are no pending changes (which<br />2770          * may free up space for us).<br />2771          */<br />2772         if (ds->ds_phys->ds_used_bytes + inflight >= ds->ds_quota) {<br />2773                 if (inflight > 0 || ds->ds_phys->ds_used_bytes <>ds_quota)<br />2774                         error = ERESTART;<br />2775                 else<br />2776                         error = EDQUOT;<br />2777         }<br />2778         mutex_exit(&ds->ds_lock);<br />2779<br />2780         return (error);<br />2781 }</span><br />